//PM Modif
var frameworkVersion = 2024;
//End PM Modif
var msfssdk = (function (exports) {
    'use strict';

    /**
     * A {@link Subscription} which executes a handler function every time it receives a notification.
     */
    class HandlerSubscription {
        /**
         * Constructor.
         * @param handler This subscription's handler. The handler will be called each time this subscription receives a
         * notification from its source.
         * @param initialNotifyFunc A function which sends initial notifications to this subscription. If not defined, this
         * subscription will not support initial notifications.
         * @param onDestroy A function which is called when this subscription is destroyed.
         */
        constructor(handler, initialNotifyFunc, onDestroy) {
            this.handler = handler;
            this.initialNotifyFunc = initialNotifyFunc;
            this.onDestroy = onDestroy;
            // Note: isAlive and isPaused used to be getters (to avoid mutation from consumers)
            //       but this has a non negligeable overhead in the publishers hotpath.
            //       So instead, the two properties are declared readonly, but will actually be mutated
            //       by this class ONLY, using casts.
            /** @inheritdoc */
            this.isAlive = true;
            /**
             * Whether this subscription is paused. Paused subscriptions do not receive notifications from their sources until
             * they are resumed.
             *
             * Note that `!isAlive` implies `isPaused` for `HandlerSubscription`
             * @override
             */
            this.isPaused = false;
            this.canInitialNotify = initialNotifyFunc !== undefined;
        }
        /**
         * Sends an initial notification to this subscription.
         * @throws Error if this subscription is not alive.
         */
        initialNotify() {
            if (!this.isAlive) {
                throw new Error('HandlerSubscription: cannot notify a dead Subscription.');
            }
            this.initialNotifyFunc && this.initialNotifyFunc(this);
        }
        /** @inheritdoc */
        pause() {
            if (!this.isAlive) {
                throw new Error('Subscription: cannot pause a dead Subscription.');
            }
            this.isPaused = true; // See comment at definition for cast info
            return this;
        }
        /** @inheritdoc */
        resume(initialNotify = false) {
            if (!this.isAlive) {
                throw new Error('Subscription: cannot resume a dead Subscription.');
            }
            if (!this.isPaused) {
                return this;
            }
            this.isPaused = false; // See comment at definition for cast info
            if (initialNotify) {
                this.initialNotify();
            }
            return this;
        }
        /** @inheritdoc */
        destroy() {
            if (!this.isAlive) {
                return;
            }
            this.isAlive = false; // See comment at definition for cast info
            // Note: We rely on dead subscriptions to be paused to avoid checking both
            this.isPaused = true; // See comment at definition for cast info
            this.onDestroy && this.onDestroy(this);
        }
    }

    /**
     * A basic implementation of {@link Consumer}.
     */
    class BasicConsumer {
        /**
         * Creates an instance of a Consumer.
         * @param subscribe A function which subscribes a handler to the source of this consumer's events.
         * @param state The state for the consumer to track.
         * @param currentHandler The current build filter handler stack, if any.
         */
        constructor(subscribe, state = {}, currentHandler) {
            this.subscribe = subscribe;
            this.state = state;
            this.currentHandler = currentHandler;
            /** @inheritdoc */
            this.isConsumer = true;
        }
        /** @inheritdoc */
        handle(handler, paused = false) {
            let activeHandler;
            if (this.currentHandler !== undefined) {
                /**
                 * The handler reference to store.
                 * @param data The input data to the handler.
                 */
                activeHandler = (data) => {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    this.currentHandler(data, this.state, handler);
                };
            }
            else {
                activeHandler = handler;
            }
            return new ConsumerSubscription(this.subscribe(activeHandler, paused));
        }
        /** @inheritdoc */
        atFrequency(frequency, immediateFirstPublish = true) {
            const initialState = {
                previousTime: Date.now(),
                firstRun: immediateFirstPublish
            };
            return new BasicConsumer(this.subscribe, initialState, this.getAtFrequencyHandler(frequency));
        }
        /**
         * Gets a handler function for a 'atFrequency' filter.
         * @param frequency The frequency, in Hz, to cap to.
         * @returns A handler function for a 'atFrequency' filter.
         */
        getAtFrequencyHandler(frequency) {
            const deltaTimeTrigger = 1000 / frequency;
            return (data, state, next) => {
                const currentTime = Date.now();
                const deltaTime = currentTime - state.previousTime;
                if (deltaTimeTrigger <= deltaTime || state.firstRun) {
                    while ((state.previousTime + deltaTimeTrigger) < currentTime) {
                        state.previousTime += deltaTimeTrigger;
                    }
                    if (state.firstRun) {
                        state.firstRun = false;
                    }
                    this.with(data, next);
                }
            };
        }
        /** @inheritdoc */
        withPrecision(precision) {
            return new BasicConsumer(this.subscribe, { lastValue: 0, hasLastValue: false }, this.getWithPrecisionHandler(precision));
        }
        /**
         * Gets a handler function for a 'withPrecision' filter.
         * @param precision The decimal precision to snap to.
         * @returns A handler function for a 'withPrecision' filter.
         */
        getWithPrecisionHandler(precision) {
            return (data, state, next) => {
                const dataValue = data;
                const multiplier = Math.pow(10, precision);
                const currentValueAtPrecision = Math.round(dataValue * multiplier) / multiplier;
                if (!state.hasLastValue || currentValueAtPrecision !== state.lastValue) {
                    state.hasLastValue = true;
                    state.lastValue = currentValueAtPrecision;
                    this.with(currentValueAtPrecision, next);
                }
            };
        }
        /** @inheritdoc */
        whenChangedBy(amount) {
            return new BasicConsumer(this.subscribe, { lastValue: 0, hasLastValue: false }, this.getWhenChangedByHandler(amount));
        }
        /**
         * Gets a handler function for a 'whenChangedBy' filter.
         * @param amount The minimum amount threshold below which the consumer will not consume.
         * @returns A handler function for a 'whenChangedBy' filter.
         */
        getWhenChangedByHandler(amount) {
            return (data, state, next) => {
                const dataValue = data;
                const diff = Math.abs(dataValue - state.lastValue);
                if (!state.hasLastValue || diff >= amount) {
                    state.hasLastValue = true;
                    state.lastValue = dataValue;
                    this.with(data, next);
                }
            };
        }
        /** @inheritdoc */
        whenChanged() {
            return new BasicConsumer(this.subscribe, { lastValue: '', hasLastValue: false }, this.getWhenChangedHandler());
        }
        /**
         * Gets a handler function for a 'whenChanged' filter.
         * @returns A handler function for a 'whenChanged' filter.
         */
        getWhenChangedHandler() {
            return (data, state, next) => {
                if (!state.hasLastValue || state.lastValue !== data) {
                    state.hasLastValue = true;
                    state.lastValue = data;
                    this.with(data, next);
                }
            };
        }
        /** @inheritdoc */
        onlyAfter(deltaTime) {
            return new BasicConsumer(this.subscribe, { previousTime: Date.now() }, this.getOnlyAfterHandler(deltaTime));
        }
        /**
         * Gets a handler function for an 'onlyAfter' filter.
         * @param deltaTime The minimum delta time between events.
         * @returns A handler function for an 'onlyAfter' filter.
         */
        getOnlyAfterHandler(deltaTime) {
            return (data, state, next) => {
                const currentTime = Date.now();
                const timeDiff = currentTime - state.previousTime;
                if (timeDiff > deltaTime) {
                    state.previousTime += deltaTime;
                    this.with(data, next);
                }
            };
        }
        /**
         * Builds a handler stack from the current handler.
         * @param data The data to send in to the handler.
         * @param handler The handler to use for processing.
         */
        with(data, handler) {
            if (this.currentHandler !== undefined) {
                this.currentHandler(data, this.state, handler);
            }
            else {
                handler(data);
            }
        }
    }
    /**
     * A {@link Subscription} for a {@link BasicConsumer}.
     */
    class ConsumerSubscription {
        /** @inheritdoc */
        get isAlive() {
            return this.sub.isAlive;
        }
        /** @inheritdoc */
        get isPaused() {
            return this.sub.isPaused;
        }
        /** @inheritdoc */
        get canInitialNotify() {
            return this.sub.canInitialNotify;
        }
        /**
         * Constructor.
         * @param sub The event bus subscription backing this subscription.
         */
        constructor(sub) {
            this.sub = sub;
        }
        /** @inheritdoc */
        pause() {
            this.sub.pause();
            return this;
        }
        /** @inheritdoc */
        resume(initialNotify = false) {
            this.sub.resume(initialNotify);
            return this;
        }
        /** @inheritdoc */
        destroy() {
            this.sub.destroy();
        }
    }

    /**
     * A typed container for subscribers interacting with the Event Bus.
     */
    class EventSubscriber {
        /**
         * Creates an instance of an EventSubscriber.
         * @param bus The EventBus that is the parent of this instance.
         */
        constructor(bus) {
            this.bus = bus;
        }
        /**
         * Subscribes to a topic on the bus.
         * @param topic The topic to subscribe to.
         * @returns A consumer to bind the event handler to.
         */
        on(topic) {
            return new BasicConsumer((handler, paused) => {
                return this.bus.on(topic, handler, paused);
            });
        }
    }

    /// <reference types="@microsoft/msfs-types/js/common" />
    /**
     * An event bus that can be used to publish data from backend
     * components and devices to consumers.
     */
    class EventBus {
        /**
         * Creates an instance of an EventBus.
         * @param useAlternativeEventSync Whether or not to use generic listener event sync (default false).
         * If true, FlowEventSync will only work for gauges.
         * @param shouldResync Whether the eventbus should ask for a resync of all previously cached events (default true)
         */
        constructor(useAlternativeEventSync = false, shouldResync = true) {
            this._topicSubsMap = new Map();
            this._wildcardSubs = new Array();
            this._wildcardNotifyDepth = 0;
            this._eventCache = new Map();
            this.onWildcardSubDestroyedFunc = this.onWildcardSubDestroyed.bind(this);
            this._busId = Math.floor(Math.random() * 2147483647);
            // fallback to flowevent when genericdatalistener not avail (su9)
            useAlternativeEventSync = (typeof RegisterGenericDataListener === 'undefined');
            const syncFunc = useAlternativeEventSync ? EventBusFlowEventSync : EventBusListenerSync;
            this._busSync = new syncFunc(this.pub.bind(this), this._busId);
            if (shouldResync === true) {
                this.syncEvent('event_bus', 'resync_request', false);
                this.on('event_bus', (data) => {
                    if (data == 'resync_request') {
                        this.resyncEvents();
                    }
                });
            }
        }
        /**
         * Subscribes to a topic on the bus.
         * @param topic The topic to subscribe to.
         * @param handler The handler to be called when an event happens.
         * @param paused Whether the new subscription should be initialized as paused. Defaults to `false`.
         * @returns The new subscription.
         */
        on(topic, handler, paused = false) {
            let subs = this._topicSubsMap.get(topic);
            if (subs === undefined) {
                subs = { handlerSubscriptions: [], notifyDepth: 0 };
                this._topicSubsMap.set(topic, subs);
                this.pub('event_bus_topic_first_sub', topic, false, false);
            }
            const initialNotifyFunc = (sub) => {
                const lastState = this._eventCache.get(topic);
                if (lastState !== undefined) {
                    sub.handler(lastState.data);
                }
            };
            const onDestroyFunc = (sub) => {
                // If we are not in the middle of a notify operation, remove the subscription.
                // Otherwise, do nothing and let the post-notify clean-up code handle it.
                if (subs && !subs.notifyDepth) {
                    subs.handlerSubscriptions.splice(subs.handlerSubscriptions.indexOf(sub), 1);
                }
            };
            const sub = new HandlerSubscription(handler, initialNotifyFunc, onDestroyFunc);
            subs.handlerSubscriptions.push(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Subscribes to all topics.
         * @param handler The handler to subscribe to all events.
         * @returns The new subscription.
         */
        onAll(handler) {
            const sub = new HandlerSubscription(handler, undefined, this.onWildcardSubDestroyedFunc);
            this._wildcardSubs.push(sub);
            return sub;
        }
        /**
         * Publishes an event to the topic on the bus.
         * @param topic The topic to publish to.
         * @param data The data portion of the event.
         * @param sync Whether or not this message needs to be synced on local stoage.
         * @param isCached Whether or not this message will be resync'd across the bus on load.
         */
        pub(topic, data, sync = false, isCached = true) {
            if (isCached) {
                this._eventCache.set(topic, { data: data, synced: sync });
            }
            const subs = this._topicSubsMap.get(topic);
            if (subs !== undefined) {
                let needCleanUpSubs = false;
                const notifyDepth = subs.notifyDepth;
                subs.notifyDepth = notifyDepth + 1;
                const subsArray = subs.handlerSubscriptions;
                const len = subsArray.length;
                for (let i = 0; i < len; i++) {
                    try {
                        const sub = subsArray[i];
                        // Note: a dead HandlerSubscription is necessarily paused.
                        if (!sub.isPaused) {
                            sub.handler(data);
                        }
                        needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`EventBus: error in handler: ${error}. topic: ${topic}. data: ${data}. sync: ${sync}. isCached: ${isCached}`, { error, topic, data, sync, isCached, subs });
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                subs.notifyDepth = notifyDepth;
                if (needCleanUpSubs && notifyDepth === 0) {
                    const filteredSubs = subsArray.filter(sub => sub.isAlive);
                    subs.handlerSubscriptions = filteredSubs;
                }
            }
            // We don't know if anything is subscribed on busses in other instruments,
            // so we'll unconditionally sync if sync is true and trust that the
            // publisher knows what it's doing.
            if (sync) {
                this.syncEvent(topic, data, isCached);
            }
            // always push to wildcard handlers
            let needCleanUpSubs = false;
            this._wildcardNotifyDepth++;
            const wcLen = this._wildcardSubs.length;
            for (let i = 0; i < wcLen; i++) {
                const sub = this._wildcardSubs[i];
                if (!sub.isPaused) {
                    sub.handler(topic, data);
                }
                needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
            }
            this._wildcardNotifyDepth--;
            if (needCleanUpSubs && this._wildcardNotifyDepth === 0) {
                this._wildcardSubs = this._wildcardSubs.filter(sub => sub.isAlive);
            }
        }
        /**
         * Responds to when a wildcard subscription is destroyed.
         * @param sub The destroyed subscription.
         */
        onWildcardSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this._wildcardNotifyDepth === 0) {
                this._wildcardSubs.splice(this._wildcardSubs.indexOf(sub), 1);
            }
        }
        /**
         * Re-sync all synced events
         */
        resyncEvents() {
            for (const [topic, event] of this._eventCache) {
                if (event.synced) {
                    this.syncEvent(topic, event.data, true);
                }
            }
        }
        /**
         * Publish an event to the sync bus.
         * @param topic The topic to publish to.
         * @param data The data to publish.
         * @param isCached Whether or not this message will be resync'd across the bus on load.
         */
        syncEvent(topic, data, isCached) {
            this._busSync.sendEvent(topic, data, isCached);
        }
        /**
         * Gets a typed publisher from the event bus..
         * @returns The typed publisher.
         */
        getPublisher() {
            return this;
        }
        /**
         * Gets a typed subscriber from the event bus.
         * @returns The typed subscriber.
         */
        getSubscriber() {
            return new EventSubscriber(this);
        }
        /**
         * Get the number of subscribes for a given topic.
         * @param topic The name of the topic.
         * @returns The number of subscribers.
         **/
        getTopicSubscriberCount(topic) {
            var _a, _b;
            return (_b = (_a = this._topicSubsMap.get(topic)) === null || _a === void 0 ? void 0 : _a.handlerSubscriptions.length) !== null && _b !== void 0 ? _b : 0;
        }
        /**
         * Executes a function once for each topic with at least one subscriber.
         * @param fn The function to execute.
         */
        forEachSubscribedTopic(fn) {
            this._topicSubsMap.forEach((subs, topic) => { subs.handlerSubscriptions.length > 0 && fn(topic, subs.handlerSubscriptions.length); });
        }
    }
    /**
     * An abstract class for bus sync implementations.
     */
    class EventBusSyncBase {
        /**
         * Creates an instance of EventBusFlowEventSync.
         * @param recvEventCb A callback to execute when an event is received on the bus.
         * @param busId The ID of the bus.
         */
        constructor(recvEventCb, busId) {
            this.isPaused = false;
            this.lastEventSynced = -1;
            this.dataPackageQueue = [];
            this.recvEventCb = recvEventCb;
            this.busId = busId;
            this.hookReceiveEvent();
            /** Sends the queued up data packages */
            const sendFn = () => {
                if (!this.isPaused && this.dataPackageQueue.length > 0) {
                    // console.log(`Sending ${this.dataPackageQueue.length} packages`);
                    const syncDataPackage = {
                        busId: this.busId,
                        packagedId: Math.floor(Math.random() * 1000000000),
                        data: this.dataPackageQueue
                    };
                    if (this.executeSync(syncDataPackage)) {
                        this.dataPackageQueue.length = 0;
                    }
                    else {
                        console.warn('Failed to send sync data package');
                    }
                }
                requestAnimationFrame(sendFn);
            };
            requestAnimationFrame(sendFn);
        }
        /**
         * Processes events received and sends them onto the local bus.
         * @param syncData The data package to process.
         */
        processEventsReceived(syncData) {
            if (this.busId !== syncData.busId) {
                // HINT: coherent events are still received twice, so check for this
                if (this.lastEventSynced !== syncData.packagedId) {
                    this.lastEventSynced = syncData.packagedId;
                    syncData.data.forEach((data) => {
                        try {
                            this.recvEventCb(data.topic, data.data !== undefined ? data.data : undefined, false, data.isCached);
                        }
                        catch (e) {
                            console.error(e);
                            if (e instanceof Error) {
                                console.error(e.stack);
                            }
                        }
                    });
                }
            }
        }
        /**
         * Sends an event via flow events.
         * @param topic The topic to send data on.
         * @param data The data to send.
         * @param isCached Whether or not this event is cached.
         */
        sendEvent(topic, data, isCached) {
            // stringify data
            const dataObj = data;
            // build a data package
            const dataPackage = {
                topic: topic,
                data: dataObj,
                isCached: isCached
            };
            // queue data package
            this.dataPackageQueue.push(dataPackage);
        }
    }
    /**
     * A class that manages event bus synchronization via Flow Event Triggers.
     * DON'T USE this, it has bad performance implications.
     * @deprecated
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    class EventBusCoherentSync extends EventBusSyncBase {
        /** @inheritdoc */
        executeSync(syncDataPackage) {
            // HINT: Stringifying the data again to circumvent the bad perf on Coherent interop
            try {
                this.listener.triggerToAllSubscribers(EventBusCoherentSync.EB_KEY, JSON.stringify(syncDataPackage));
                return true;
            }
            catch (error) {
                return false;
            }
        }
        /** @inheritdoc */
        hookReceiveEvent() {
            this.listener = RegisterViewListener(EventBusCoherentSync.EB_LISTENER_KEY, undefined, true);
            this.listener.on(EventBusCoherentSync.EB_KEY, (e) => {
                try {
                    const evt = JSON.parse(e);
                    this.processEventsReceived(evt);
                }
                catch (error) {
                    console.error(error);
                }
            });
        }
    }
    EventBusCoherentSync.EB_KEY = 'eb.evt';
    EventBusCoherentSync.EB_LISTENER_KEY = 'JS_LISTENER_SIMVARS';
    /**
     * A class that manages event bus synchronization via Flow Event Triggers.
     */
    class EventBusFlowEventSync extends EventBusSyncBase {
        /** @inheritdoc */
        executeSync(syncDataPackage) {
            // console.log('Sending sync package: ' + syncDataPackage.packagedId);
            try {
                LaunchFlowEvent('ON_MOUSERECT_HTMLEVENT', EventBusFlowEventSync.EB_LISTENER_KEY, this.busId.toString(), JSON.stringify(syncDataPackage));
                return true;
            }
            catch (error) {
                return false;
            }
        }
        /** @inheritdoc */
        hookReceiveEvent() {
            Coherent.on('OnInteractionEvent', (target, args) => {
                // identify if its a busevent
                if (args.length === 0 || args[0] !== EventBusFlowEventSync.EB_LISTENER_KEY || !args[2]) {
                    return;
                }
                this.processEventsReceived(JSON.parse(args[2]));
            });
        }
    }
    EventBusFlowEventSync.EB_LISTENER_KEY = 'EB_EVENTS';
    //// END GLOBALS DECLARATION
    /**
     * A class that manages event bus synchronization via the Generic Data Listener.
     */
    class EventBusListenerSync extends EventBusSyncBase {
        /** @inheritdoc */
        executeSync(syncDataPackage) {
            try {
                this.listener.send(EventBusListenerSync.EB_KEY, syncDataPackage);
                return true;
            }
            catch (error) {
                return false;
            }
        }
        /** @inheritdoc */
        hookReceiveEvent() {
            // pause the sync until the listener is ready
            this.isPaused = true;
            this.listener = RegisterGenericDataListener(() => {
                this.listener.onDataReceived(EventBusListenerSync.EB_KEY, (data) => {
                    try {
                        this.processEventsReceived(data);
                    }
                    catch (error) {
                        console.error(error);
                    }
                });
                this.isPaused = false;
            });
        }
    }
    EventBusListenerSync.EB_KEY = 'wt.eb.evt';
    EventBusListenerSync.EB_LISTENER_KEY = 'JS_LISTENER_GENERICDATA';

    /**
     * A PublishPacer that only allows publishing on an interval.
     */
    class IntervalPacer {
        /**
         * Create an IntervalPacer.
         * @param msec Time to wait between publishs in ms
         */
        constructor(msec) {
            this.lastPublished = new Map();
            this.interval = msec;
        }
        /**
         * Determine whether the data can be published based on the time since its
         * prior publish.
         * @param topic The topic data would be sent on.
         * @param data The data which would be sent.
         * @returns A bool indicating if the data should be published.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canPublish(topic, data) {
            const prior = this.lastPublished.get(topic);
            const now = Date.now();
            if (prior && now - prior < this.interval) {
                return false;
            }
            this.lastPublished.set(topic, now);
            return true;
        }
    }
    /**
     * A PublishPacer that only allows publishing when a value has changed
     * by a specifed amount from the prior publish.
     */
    class DeltaPacer {
        /**
         * Create a DeltaPacer.
         * @param delta The difference required for publishing to be allowed.
         */
        constructor(delta) {
            this.lastPublished = new Map();
            this.delta = delta;
        }
        /**
         * Determine whether the data can be published based on its delta from the
         * pror publish.
         * @param topic The topic data would be sent on.
         * @param data The data which would be sent.
         * @returns A bool indicating if the data should be published.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canPublish(topic, data) {
            const prior = this.lastPublished.get(topic);
            if (prior && Math.abs(data - prior) < this.delta) {
                return false;
            }
            this.lastPublished.set(topic, data);
            return true;
        }
    }

    /**
     * Republishes event bus topics.
     */
    class EventRepublisher {
        /**
         * Constructor.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.metaEvents = this.bus.getSubscriber();
            this.sourceSubscriber = this.bus.getSubscriber();
            this.publisher = this.bus.getPublisher();
            this.republishes = new Map();
            this.republishId = 0;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        startRepublish(sourceTopic, targetTopic, sync, cache, map) {
            const id = this.republishId++;
            if (sync || this.bus.getTopicSubscriberCount(targetTopic) > 0) {
                this.registerRepublish(id, sourceTopic, targetTopic, sync, cache, map);
            }
            else {
                const topicSub = this.metaEvents.on('event_bus_topic_first_sub').handle(topic => {
                    if (topic === targetTopic) {
                        topicSub.destroy();
                        this.registerRepublish(id, sourceTopic, targetTopic, sync, cache, map);
                    }
                }, true);
                this.republishes.set(id, topicSub);
                topicSub.resume();
            }
            return id;
        }
        /**
         * Stops a republish handled by this publisher.
         * @param id The unique ID associated with the republish to stop.
         * @returns Whether the requested republish was stopped.
         */
        stopRepublish(id) {
            const republish = this.republishes.get(id);
            if (republish === undefined) {
                return false;
            }
            this.republishes.delete(id);
            republish.destroy();
            return true;
        }
        /**
         * Clears all republishes from this publisher.
         */
        clearRepublishes() {
            for (const republish of this.republishes.values()) {
                republish.destroy();
            }
            this.republishes.clear();
        }
        /**
         * Registers a republish.
         * @param id A unique ID to associate with the republish.
         * @param sourceTopic The source topic.
         * @param targetTopic The target topic.
         * @param sync Whether the target topic should be synced across instruments.
         * @param cache Whether the target topic data should be cached.
         * @param map A mapping function to use to transform the source data.
         */
        registerRepublish(id, sourceTopic, targetTopic, sync, cache, map) {
            const handler = map === undefined
                ? (sourceData) => { this.publisher.pub(targetTopic, sourceData, sync, cache); }
                : (sourceData) => { this.publisher.pub(targetTopic, map(sourceData), sync, cache); };
            const republish = this.sourceSubscriber.on(sourceTopic).handle(handler, true);
            this.republishes.set(id, republish);
            republish.resume(true);
        }
    }

    /**
     * Captures the state of a value from a consumer.
     */
    class ConsumerValue {
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether this object is alive. While alive, this object will update its value from its event consumer unless it
         * is paused. Once dead, this object will no longer update its value and cannot be resumed again.
         */
        get isAlive() {
            return this._isAlive;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether event consumption is currently paused. While paused, this object's value will not update.
         */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Creates an instance of a ConsumerValue.
         * @param consumer The consumer to track.
         * @param initialValue The initial value.
         */
        constructor(consumer, initialValue) {
            /** @inheritDoc */
            this.canInitialNotify = true;
            this.consumerHandler = this.onEventConsumed.bind(this);
            this.needSetDefaultValue = false;
            this._isAlive = true;
            this._isPaused = false;
            this.value = initialValue;
            this.sub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler);
        }
        /**
         * Consumes an event.
         * @param value The value of the event.
         */
        onEventConsumed(value) {
            if (this.needSetDefaultValue) {
                this.needSetDefaultValue = false;
                delete this.defaultValue;
            }
            this.value = value;
        }
        /** @inheritDoc */
        get() {
            return this.value;
        }
        /**
         * Sets the consumer from which this object derives its value. If the consumer is null, this object's value will
         * not be updated until a non-null consumer is set.
         * @param consumer An event consumer.
         * @returns This object, after its consumer has been set.
         */
        setConsumer(consumer) {
            var _a;
            if (!this._isAlive) {
                return this;
            }
            this.needSetDefaultValue = false;
            delete this.defaultValue;
            (_a = this.sub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.sub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler, this._isPaused);
            return this;
        }
        /**
         * Sets the consumer from which this object derives its value and designates a default value to set if an event is
         * not immediately consumed from the new consumer when this object is resumed. If the consumer is null, then this
         * object's value will be set to the default value.
         * @param consumer An event consumer.
         * @param defaultVal The default value to set if the new consumer is null or if an event is not immediately consumed
         * from the new consumer when this object is resumed.
         * @returns This object, after its consumer has been set.
         */
        setConsumerWithDefault(consumer, defaultVal) {
            var _a;
            if (!this._isAlive) {
                return this;
            }
            this.defaultValue = defaultVal;
            this.needSetDefaultValue = true;
            (_a = this.sub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.sub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler, this._isPaused);
            if (!this._isPaused && this.needSetDefaultValue) {
                this.value = this.defaultValue;
                this.needSetDefaultValue = false;
                delete this.defaultValue;
            }
            return this;
        }
        /**
         * Resets this subject to an initial value and optionally sets a new consumer from which this subject will derive its
         * value. If the consumer is null, then this object's value will not be updated until a non-null consumer is set.
         * @param initialVal The initial value to which to reset this object.
         * @param consumer An event consumer. Defaults to `null`.
         * @returns This object, after it has been reset.
         */
        reset(initialVal, consumer = null) {
            if (!this._isAlive) {
                return this;
            }
            this.value = initialVal;
            return this.setConsumer(consumer);
        }
        /**
         * Pauses consuming events for this object. Once paused, this object's value will not be updated.
         * @returns This object, after it has been paused.
         */
        pause() {
            var _a;
            if (this._isPaused) {
                return this;
            }
            (_a = this.sub) === null || _a === void 0 ? void 0 : _a.pause();
            this._isPaused = true;
            return this;
        }
        /**
         * Resumes consuming events for this object. Once resumed, this object's value will be updated from consumed
         * events.
         *
         * Any `initialNotify` argument passed to this method is ignored. This object is always immediately notified of its
         * event consumer's value when resumed.
         * @returns This object, after it has been resumed.
         */
        resume() {
            var _a;
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            (_a = this.sub) === null || _a === void 0 ? void 0 : _a.resume(true);
            if (this.needSetDefaultValue) {
                this.value = this.defaultValue;
                this.needSetDefaultValue = false;
                delete this.defaultValue;
            }
            return this;
        }
        /**
         * Destroys this object. Once destroyed, it will no longer consume events to update its value.
         */
        destroy() {
            var _a;
            this._isAlive = false;
            (_a = this.sub) === null || _a === void 0 ? void 0 : _a.destroy();
        }
        /**
         * Creates a new ConsumerValue.
         * @param consumer The consumer to track.
         * @param initialValue The initial value.
         * @returns The created ConsumerValue.
         */
        static create(consumer, initialValue) {
            return new ConsumerValue(consumer, initialValue);
        }
    }

    /* eslint-disable @typescript-eslint/no-duplicate-enum-values */
    /* eslint-disable @typescript-eslint/ban-ts-comment */
    /**
     * Valid type arguments for Set/GetSimVarValue
     */
    exports.SimVarValueType = void 0;
    (function (SimVarValueType) {
        SimVarValueType["Amps"] = "Amperes";
        SimVarValueType["Bool"] = "bool";
        SimVarValueType["Celsius"] = "celsius";
        SimVarValueType["CubicInches"] = "cubic inches";
        SimVarValueType["Degree"] = "degrees";
        SimVarValueType["DegreesPerSecond"] = "degrees per second";
        SimVarValueType["Enum"] = "enum";
        SimVarValueType["Farenheit"] = "farenheit";
        SimVarValueType["Feet"] = "feet";
        SimVarValueType["FPM"] = "feet per minute";
        SimVarValueType["FtLb"] = "Foot pounds";
        SimVarValueType["GAL"] = "gallons";
        SimVarValueType["GPH"] = "gph";
        SimVarValueType["Hertz"] = "hertz";
        SimVarValueType["Hours"] = "Hours";
        SimVarValueType["HPA"] = "hectopascals";
        SimVarValueType["Inches"] = "inches";
        SimVarValueType["InHG"] = "inches of mercury";
        SimVarValueType["KHz"] = "KHz";
        SimVarValueType["Knots"] = "knots";
        SimVarValueType["LBS"] = "pounds";
        SimVarValueType["Liters"] = "liters";
        SimVarValueType["LLA"] = "latlonalt";
        SimVarValueType["Mach"] = "mach";
        SimVarValueType["MB"] = "Millibars";
        SimVarValueType["Meters"] = "meters";
        SimVarValueType["MetersPerSecond"] = "meters per second";
        SimVarValueType["MetersPerSecondSquared"] = "meters per second squared";
        SimVarValueType["MillimetersWater"] = "millimeters of water";
        SimVarValueType["MHz"] = "MHz";
        SimVarValueType["NM"] = "nautical mile";
        SimVarValueType["Number"] = "number";
        SimVarValueType["Percent"] = "percent";
        SimVarValueType["PercentOver100"] = "percent over 100";
        SimVarValueType["PerSecond"] = "per second";
        SimVarValueType["Position"] = "position";
        SimVarValueType["Position16k"] = "position 16k";
        SimVarValueType["Position32k"] = "position 32k";
        SimVarValueType["Pounds"] = "pounds";
        SimVarValueType["PPH"] = "Pounds per hour";
        SimVarValueType["PSI"] = "psi";
        SimVarValueType["Radians"] = "radians";
        SimVarValueType["RadiansPerSecond"] = "radians per second";
        SimVarValueType["Rankine"] = "rankine";
        SimVarValueType["RPM"] = "Rpm";
        SimVarValueType["Seconds"] = "seconds";
        SimVarValueType["SlugsPerCubicFoot"] = "slug per cubic foot";
        SimVarValueType["String"] = "string";
        SimVarValueType["Volts"] = "Volts";
    })(exports.SimVarValueType || (exports.SimVarValueType = {}));
    const latlonaltRegEx = new RegExp(/latlonalt/i);
    const latlonaltpbhRegex = new RegExp(/latlonaltpbh/i);
    const pbhRegex = new RegExp(/pbh/i);
    const pid_structRegex = new RegExp(/pid_struct/i);
    const xyzRegex = new RegExp(/xyz/i);
    const stringRegex = new RegExp(/string/i);
    const boolRegex = new RegExp(/boolean|bool/i);
    const numberRegex = new RegExp(/number/i);
    const defaultSource = '';
    exports.SimVarStructTypes = void 0;
    (function (SimVarStructTypes) {
        SimVarStructTypes[SimVarStructTypes["Number"] = 0] = "Number";
        SimVarStructTypes[SimVarStructTypes["String"] = 1] = "String";
        SimVarStructTypes[SimVarStructTypes["LatLongAlt"] = 2] = "LatLongAlt";
        SimVarStructTypes[SimVarStructTypes["LatLongAltPbh"] = 3] = "LatLongAltPbh";
        SimVarStructTypes[SimVarStructTypes["PitchBankHeading"] = 4] = "PitchBankHeading";
        SimVarStructTypes[SimVarStructTypes["PidStruct"] = 5] = "PidStruct";
        SimVarStructTypes[SimVarStructTypes["XYZ"] = 6] = "XYZ";
        SimVarStructTypes[SimVarStructTypes["Boolean"] = 7] = "Boolean";
    })(exports.SimVarStructTypes || (exports.SimVarStructTypes = {}));
    /**
     * Converts a SimVar unit type to a SimVar value struct type.
     * @param unit The SimVar unit type for which to get a struct type.
     * @param coerceBooleanType Whether the `bool/boolean` SimVar unit type should be converted to the `Boolean` struct
     * type instead of the `Number` struct type. Using the `Boolean` struct type will cause SimVar values to be coerced to
     * the `boolean` type using the `!!` operator. Defaults to `false`.
     * @returns The SimVar value struct type corresponding to the specified unit type.
     */
    function GetSimVarStructType(unit, coerceBooleanType = false) {
        if (numberRegex.test(unit)) {
            return exports.SimVarStructTypes.Number;
        }
        else if (stringRegex.test(unit)) {
            return exports.SimVarStructTypes.String;
        }
        else if (latlonaltRegEx.test(unit)) {
            return exports.SimVarStructTypes.LatLongAlt;
        }
        else if (latlonaltpbhRegex.test(unit)) {
            return exports.SimVarStructTypes.LatLongAltPbh;
        }
        else if (pbhRegex.test(unit)) {
            return exports.SimVarStructTypes.PitchBankHeading;
        }
        else if (pid_structRegex.test(unit)) {
            return exports.SimVarStructTypes.PidStruct;
        }
        else if (xyzRegex.test(unit)) {
            return exports.SimVarStructTypes.XYZ;
        }
        else if (boolRegex.test(unit) && coerceBooleanType) {
            return exports.SimVarStructTypes.Boolean;
        }
        else {
            return exports.SimVarStructTypes.Number;
        }
    }
    // @ts-ignore
    SimVar.GetSimVarValue = (name, unit, dataSource = defaultSource) => {
        try {
            if (simvar) {
                let output;
                const registeredID = SimVar.GetRegisteredId(name, unit, dataSource);
                if (registeredID >= 0) {
                    if (numberRegex.test(unit)) {
                        output = simvar.getValueReg(registeredID);
                    }
                    else if (stringRegex.test(unit)) {
                        output = simvar.getValueReg_String(registeredID);
                    }
                    else if (latlonaltRegEx.test(unit)) {
                        output = new LatLongAlt(simvar.getValue_LatLongAlt(name, dataSource));
                    }
                    else if (latlonaltpbhRegex.test(unit)) {
                        output = new LatLongAltPBH(simvar.getValue_LatLongAltPBH(name, dataSource));
                    }
                    else if (pbhRegex.test(unit)) {
                        output = new PitchBankHeading(simvar.getValue_PBH(name, dataSource));
                    }
                    else if (pid_structRegex.test(unit)) {
                        output = new PID_STRUCT(simvar.getValue_PID_STRUCT(name, dataSource));
                    }
                    else if (xyzRegex.test(unit)) {
                        output = new XYZ(simvar.getValue_XYZ(name, dataSource));
                    }
                    else {
                        output = simvar.getValueReg(registeredID);
                    }
                }
                return output;
            }
            else {
                console.warn('SimVar handler is not defined (' + name + ')');
            }
        }
        catch (error) {
            console.warn('ERROR ', error, ' GetSimVarValue ' + name + ' unit : ' + unit);
            return null;
        }
        return null;
    };
    /**
     * Resolves a SimVar definition into a registered definition. The registered definition can be used with
     * {@link GetRegisteredSimVarValue | GetRegisteredSimVarValue()} for efficient retrieval of SimVar values.
     * @param simVarDef The SimVar definition to resolve.
     * @param coerceBooleanType Whether to coerce values retrieved using the `bool/boolean` SimVar type to the `boolean`
     * type. If `true`, then type coercion will be performed using the "boolean cast" operator (`!!`). If `false`, then
     * type coercion will not be performed and values will be left typed as `number`. Defaults to `false`.
     * @returns The resolved registered SimVar definition corresponding to the specified SimVar definition.
     */
    function ResolveRegisteredSimVar(simVarDef, coerceBooleanType = false) {
        return {
            name: simVarDef.name,
            registeredID: SimVar.GetRegisteredId(simVarDef.name, simVarDef.type, defaultSource),
            structType: GetSimVarStructType(simVarDef.type, coerceBooleanType)
        };
    }
    /**
     * Gets the value of a SimVar using a registered SimVar definition. This function is more performant than
     * `SimVar.GetSimVarValue()`.
     * @param regSimVarDef The registered SimVar definition describing the value to get.
     * @returns The value of the SimVar described by the specified registered SimVar definition, or `null` if the value
     * could not be retrieved.
     */
    function GetRegisteredSimVarValue(regSimVarDef) {
        try {
            if (regSimVarDef.registeredID >= 0) {
                switch (regSimVarDef.structType) {
                    case exports.SimVarStructTypes.Number:
                        return simvar.getValueReg(regSimVarDef.registeredID);
                    case exports.SimVarStructTypes.String:
                        return simvar.getValueReg_String(regSimVarDef.registeredID);
                    case exports.SimVarStructTypes.LatLongAlt:
                        return new LatLongAlt(simvar.getValue_LatLongAlt(regSimVarDef.name, defaultSource));
                    case exports.SimVarStructTypes.LatLongAltPbh:
                        return new LatLongAltPBH(simvar.getValue_LatLongAltPBH(regSimVarDef.name, defaultSource));
                    case exports.SimVarStructTypes.PitchBankHeading:
                        return new PitchBankHeading(simvar.getValue_PBH(regSimVarDef.name, defaultSource));
                    case exports.SimVarStructTypes.PidStruct:
                        return new PID_STRUCT(simvar.getValue_PID_STRUCT(regSimVarDef.name, defaultSource));
                    case exports.SimVarStructTypes.XYZ:
                        return new XYZ(simvar.getValue_XYZ(regSimVarDef.name, defaultSource));
                    case exports.SimVarStructTypes.Boolean:
                        return !!simvar.getValueReg(regSimVarDef.registeredID);
                    default: return null;
                }
            }
        }
        catch (error) {
            console.warn('ERROR ', error, ' GetSimVarValue ' + regSimVarDef.name + ' structType : ' + regSimVarDef.structType);
        }
        return null;
    }
    // @ts-ignore
    SimVar.SetSimVarValue = (name, unit, value, dataSource = defaultSource) => {
        if (value == undefined) {
            console.warn(name + ' : Trying to set a null value');
            return Promise.resolve();
        }
        try {
            if (simvar) {
                const regID = SimVar.GetRegisteredId(name, unit, dataSource);
                if (regID >= 0) {
                    if (stringRegex.test(unit)) {
                        return Coherent.call('setValueReg_String', regID, value);
                    }
                    else if (boolRegex.test(unit)) {
                        return Coherent.call('setValueReg_Bool', regID, !!value);
                    }
                    else if (numberRegex.test(unit)) {
                        return Coherent.call('setValueReg_Number', regID, value);
                    }
                    else if (latlonaltRegEx.test(unit)) {
                        return Coherent.call('setValue_LatLongAlt', name, value, dataSource);
                    }
                    else if (latlonaltpbhRegex.test(unit)) {
                        return Coherent.call('setValue_LatLongAltPBH', name, value, dataSource);
                    }
                    else if (pbhRegex.test(unit)) {
                        return Coherent.call('setValue_PBH', name, value, dataSource);
                    }
                    else if (pid_structRegex.test(unit)) {
                        return Coherent.call('setValue_PID_STRUCT', name, value, dataSource);
                    }
                    else if (xyzRegex.test(unit)) {
                        return Coherent.call('setValue_XYZ', name, value, dataSource);
                    }
                    else {
                        return Coherent.call('setValueReg_Number', regID, value);
                    }
                }
            }
            else {
                console.warn('SimVar handler is not defined');
            }
        }
        catch (error) {
            console.warn('error SetSimVarValue ' + error);
        }
        return Promise.resolve();
    };
    // @ts-ignore
    const SimOvrd = {
        GetSimVarValue: SimVar.GetSimVarValue,
        SetSimVarValue: SimVar.SetSimVarValue,
    };

    /**
     * A basic event-bus publisher.
     */
    class BasePublisher {
        /**
         * Creates an instance of BasePublisher.
         * @param bus The common event bus.
         * @param pacer An optional pacer to control the rate of publishing.
         */
        constructor(bus, pacer = undefined) {
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.publishActive = false;
            this.pacer = pacer;
        }
        /**
         * Start publishing.
         */
        startPublish() {
            this.publishActive = true;
        }
        /**
         * Stop publishing.
         */
        stopPublish() {
            this.publishActive = false;
        }
        /**
         * Tells whether or not the publisher is currently active.
         * @returns True if the publisher is active, false otherwise.
         */
        isPublishing() {
            return this.publishActive;
        }
        /**
         * A callback called when the publisher receives an update cycle.
         */
        onUpdate() {
            return;
        }
        /**
         * Publish a message if publishing is acpive
         * @param topic The topic key to publish to.
         * @param data The data type for chosen topic.
         * @param sync Whether or not the event should be synced to other instruments. Defaults to `false`.
         * @param isCached Whether or not the event should be cached. Defaults to `true`.
         */
        publish(topic, data, sync = false, isCached = true) {
            if (this.publishActive && (!this.pacer || this.pacer.canPublish(topic, data))) {
                this.publisher.pub(topic, data, sync, isCached);
            }
        }
    }
    /**
     * A publisher that sends a constant stream of random numbers.
     */
    class RandomNumberPublisher extends BasePublisher {
        /**
         * Start publishing random numbers.
         */
        startPublish() {
            super.startPublish();
            this.publishRandomNumbers();
        }
        /**
         * Async thread that publishes random numbers
         * @param ms - Milliseconds to sleep between publishes
         */
        async publishRandomNumbers(ms = 1000) {
            while (this.isPublishing()) {
                const newVal = Math.floor(Math.random() * ms);
                this.publish('randomNumber', newVal, true);
                await new Promise(r => setTimeout(r, ms));
            }
        }
    }
    /**
     * A base class for publishers that need to handle simvars with built-in
     * support for pacing callbacks.
     */
    class SimVarPublisher extends BasePublisher {
        /**
         * Creates a new instance of SimVarPublisher.
         * @param entries Entries describing the SimVars to publish.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to control the rate of publishing.
         */
        constructor(entries, bus, pacer) {
            super(bus, pacer);
            this.resolvedSimVars = new Map();
            this.indexedSimVars = new Map();
            this.subscribed = new Map();
            for (const [topic, entry] of entries) {
                if (entry.indexed) {
                    this.indexedSimVars.set(topic, {
                        name: entry.name,
                        type: entry.type,
                        map: entry.map,
                        indexes: entry.indexed === true ? undefined : new Set(entry.indexed),
                        defaultIndex: entry.defaultIndex
                    });
                }
                else {
                    this.resolvedSimVars.set(topic, Object.assign({}, entry));
                }
            }
            const handleSubscribedTopic = this.handleSubscribedTopic.bind(this);
            // Iterate over each subscribed topic on the bus to see if it matches any of our topics. If so, start publishing.
            this.bus.forEachSubscribedTopic(handleSubscribedTopic);
            // Listen to first-time topic subscriptions. If any of them match our topics, start publishing.
            this.bus.getSubscriber().on('event_bus_topic_first_sub').handle(handleSubscribedTopic);
        }
        /**
         * Handles when an event bus topic is subscribed to for the first time.
         * @param topic The subscribed topic.
         */
        handleSubscribedTopic(topic) {
            if (this.resolvedSimVars.has(topic)) {
                // If topic matches an already resolved topic -> start publishing.
                this.onTopicSubscribed(topic);
            }
            else {
                // Check if topic matches indexed topic.
                this.tryMatchIndexedSubscribedTopic(topic);
            }
        }
        /**
         * Checks if a subscribed topic matches one of this publisher's indexed topics, and if so resolves and starts
         * publishing the indexed topic.
         * @param topic The subscribed topic to check.
         */
        tryMatchIndexedSubscribedTopic(topic) {
            var _a;
            if (this.indexedSimVars.size === 0) {
                return;
            }
            let entry = this.indexedSimVars.get(topic);
            if (entry) {
                // The subscribed topic matches an unsuffixed topic -> check if the unsuffixed topic should be published and if
                // so, resolve the default index.
                if (entry.defaultIndex !== null) {
                    const resolved = this.resolveIndexedSimVar(topic, entry, (_a = entry.defaultIndex) !== null && _a !== void 0 ? _a : 1);
                    if (resolved !== undefined) {
                        this.onTopicSubscribed(resolved);
                    }
                }
                return;
            }
            if (!SimVarPublisher.INDEXED_REGEX.test(topic)) { // Don't generate an array if we don't have to.
                return;
            }
            const match = topic.match(SimVarPublisher.INDEXED_REGEX);
            const [, matchedTopic, index] = match;
            entry = this.indexedSimVars.get(matchedTopic);
            if (entry) {
                const resolved = this.resolveIndexedSimVar(matchedTopic, entry, parseInt(index));
                if (resolved !== undefined) {
                    this.onTopicSubscribed(resolved);
                }
            }
        }
        /**
         * Attempts to resolve an indexed topic with an index, generating a version of the topic which is mapped to an
         * indexed simvar. The resolved indexed topic can then be published.
         * @param topic The topic to resolve.
         * @param entry The entry of the topic to resolve.
         * @param index The index with which to resolve the topic. If not defined, the topic will resolve to itself (without
         * a suffix) and will be mapped the index-1 version of its simvar.
         * @returns The resolved indexed topic, or `undefined` if the topic could not be resolved with the specified index.
         */
        resolveIndexedSimVar(topic, entry, index) {
            index !== null && index !== void 0 ? index : (index = 1);
            const resolvedTopic = `${topic}_${index}`;
            if (this.resolvedSimVars.has(resolvedTopic)) {
                return resolvedTopic;
            }
            const defaultIndex = entry.defaultIndex === undefined ? 1 : entry.defaultIndex;
            // Ensure that the index we are trying to resolve is a valid index for the topic.
            if (entry.indexes !== undefined && !entry.indexes.has(index)) {
                return undefined;
            }
            this.resolvedSimVars.set(resolvedTopic, {
                name: entry.name.replace('#index#', `${index !== null && index !== void 0 ? index : 1}`),
                type: entry.type,
                map: entry.map,
                unsuffixedTopic: defaultIndex === index ? topic : undefined,
            });
            return resolvedTopic;
        }
        /**
         * Responds to when one of this publisher's topics is subscribed to for the first time.
         * @param topic The topic that was subscribed to.
         */
        onTopicSubscribed(topic) {
            if (this.subscribed.has(topic)) {
                return;
            }
            const resSimVar = this.resolvedSimVars.get(topic);
            resSimVar.registeredDef = ResolveRegisteredSimVar(resSimVar, true);
            this.subscribed.set(topic, resSimVar);
            // Immediately publish the current value if publishing is active.
            if (this.publishActive) {
                this.publishTopic(topic, resSimVar);
            }
        }
        /**
         * Publish all subscribed data points to the bus.
         */
        onUpdate() {
            for (const topicAndEntry of this.subscribed) {
                this.publishTopic(topicAndEntry[0], topicAndEntry[1]);
            }
        }
        /**
         * Publishes data to the event bus for a topic.
         * @param topic The topic to publish.
         * @param entry The entry corresponding to the topic
         */
        publishTopic(topic, entry) {
            if (entry) {
                const value = this.getValueFromEntry(entry);
                this.publish(topic, value);
                // Check if we need to publish the same value to the unsuffixed version of the topic.
                if (entry.unsuffixedTopic) {
                    this.publish(entry.unsuffixedTopic, value);
                }
            }
        }
        /**
         * Gets the current value for a topic.
         * @param topic A topic.
         * @returns The current value for the specified topic.
         */
        getValue(topic) {
            const entry = this.resolvedSimVars.get(topic);
            if (entry === undefined) {
                return undefined;
            }
            return this.getValueFromEntry(entry);
        }
        /**
         * Gets the current value for a resolved topic entry.
         * @param entry An entry for a resolved topic.
         * @returns The current value for the specified entry.
         */
        getValueFromEntry(entry) {
            return entry.map === undefined
                ? GetRegisteredSimVarValue(entry.registeredDef)
                : entry.map(GetRegisteredSimVarValue(entry.registeredDef));
        }
    }
    SimVarPublisher.INDEXED_REGEX = /(.*)_(0|[1-9]\d*)$/;
    /**
     * A base class for publishers that need to handle simvars with built-in
     * support for pacing callbacks.
     */
    class GameVarPublisher extends BasePublisher {
        /**
         * Create a SimVarPublisher
         * @param simVarMap A map of simvar event type keys to a SimVarDefinition.
         * @param bus The EventBus to use for publishing.
         * @param pacer An optional pacer to control the rate of publishing.
         */
        constructor(simVarMap, bus, pacer) {
            super(bus, pacer);
            this.simvars = simVarMap;
            this.subscribed = new Set();
            // Start polling all simvars for which there are existing subscriptions.
            for (const topic of this.simvars.keys()) {
                if (bus.getTopicSubscriberCount(topic) > 0) {
                    this.onTopicSubscribed(topic);
                }
            }
            bus.getSubscriber().on('event_bus_topic_first_sub').handle((topic) => {
                if (this.simvars.has(topic)) {
                    this.onTopicSubscribed(topic);
                }
            });
        }
        /**
         * Responds to when one of this publisher's topics is subscribed to for the first time.
         * @param topic The topic that was subscribed to.
         */
        onTopicSubscribed(topic) {
            if (this.subscribed.has(topic)) {
                return;
            }
            this.subscribed.add(topic);
            // Immediately publish the current value if publishing is active.
            if (this.publishActive) {
                this.publishTopic(topic);
            }
        }
        /**
         * Publish all subscribed data points to the bus.
         */
        onUpdate() {
            for (const topic of this.subscribed.values()) {
                this.publishTopic(topic);
            }
        }
        /**
         * Publishes data to the event bus for a topic.
         * @param topic The topic to publish.
         */
        publishTopic(topic) {
            const value = this.getValue(topic);
            if (value !== undefined) {
                this.publish(topic, value);
            }
        }
        /**
         * Gets the current value for a topic.
         * @param topic A topic.
         * @returns The current value for the specified topic.
         */
        getValue(topic) {
            const entry = this.simvars.get(topic);
            if (entry === undefined) {
                return undefined;
            }
            return entry.map === undefined
                ? this.getGameVarValue(entry)
                : entry.map(this.getGameVarValue(entry));
        }
        /**
         * Gets the value of the SimVar
         * @param entry The SimVar definition entry
         * @returns The value of the SimVar
         */
        getGameVarValue(entry) {
            const svValue = SimVar.GetGameVarValue(entry.name, entry.type);
            if (entry.type === exports.SimVarValueType.Bool) {
                return svValue === 1;
            }
            return svValue;
        }
    }

    /**
     * A publisher for publishing H:Events on the bus.
     */
    class HEventPublisher extends BasePublisher {
        /**
         * Dispatches an H:Event to the event bus.
         * @param hEvent The H:Event to dispatch.
         * @param sync Whether this event should be synced (optional, default false)
         */
        dispatchHEvent(hEvent, sync = false) {
            // console.log(`dispaching hevent:  ${hEvent}`);
            this.publish('hEvent', hEvent, sync, false);
        }
    }

    /**
     * A pipe from an input subscribable to an output mutable subscribable. Each notification received by the pipe is used
     * to change the state of the output subscribable.
     */
    class SubscribablePipe extends HandlerSubscription {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(from, to, arg3, arg4) {
            let handler;
            let onDestroy;
            if (typeof arg4 === 'function') {
                handler = (fromVal) => {
                    to.set(arg3(fromVal, to.get()));
                };
                onDestroy = arg4;
            }
            else {
                handler = (fromVal) => {
                    to.set(fromVal);
                };
                onDestroy = arg3;
            }
            super(handler, (sub) => { sub.handler(from.get()); }, onDestroy);
        }
    }

    /**
     * An abstract implementation of a subscribable which allows adding, removing, and notifying subscribers.
     */
    class AbstractSubscribable {
        constructor() {
            this.isSubscribable = true;
            this.notifyDepth = 0;
            /** A function which sends initial notifications to subscriptions. */
            this.initialNotifyFunc = this.notifySubscription.bind(this);
            /** A function which responds to when a subscription to this subscribable is destroyed. */
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /**
         * Adds a subscription to this subscribable.
         * @param sub The subscription to add.
         */
        addSubscription(sub) {
            if (this.subs) {
                this.subs.push(sub);
            }
            else if (this.singletonSub) {
                this.subs = [this.singletonSub, sub];
                delete this.singletonSub;
            }
            else {
                this.singletonSub = sub;
            }
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Notifies subscriptions that this subscribable's value has changed.
         */
        notify() {
            const canCleanUpSubs = this.notifyDepth === 0;
            let needCleanUpSubs = false;
            this.notifyDepth++;
            if (this.singletonSub) {
                try {
                    if (!this.singletonSub.isPaused) {
                        this.notifySubscription(this.singletonSub);
                    }
                }
                catch (error) {
                    console.error(`AbstractSubscribable: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
                if (canCleanUpSubs) {
                    // If subscriptions were added during the notification, then singletonSub would be deleted and replaced with
                    // the subs array.
                    if (this.singletonSub) {
                        needCleanUpSubs = !this.singletonSub.isAlive;
                    }
                    else if (this.subs) {
                        for (let i = 0; i < this.subs.length; i++) {
                            if (!this.subs[i].isAlive) {
                                needCleanUpSubs = true;
                                break;
                            }
                        }
                    }
                }
            }
            else if (this.subs) {
                const subLen = this.subs.length;
                for (let i = 0; i < subLen; i++) {
                    try {
                        const sub = this.subs[i];
                        if (!sub.isPaused) {
                            this.notifySubscription(sub);
                        }
                        needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`AbstractSubscribable: error in handler: ${error}`);
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                // If subscriptions were added during the notification and a cleanup operation is not already pending, then we
                // need to check if any of the new subscriptions are already dead and if so, pend a cleanup operation.
                if (canCleanUpSubs && !needCleanUpSubs) {
                    for (let i = subLen; i < this.subs.length; i++) {
                        if (!this.subs[i].isAlive) {
                            needCleanUpSubs = true;
                            break;
                        }
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs) {
                if (this.singletonSub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    this.subs = this.subs.filter(sub => sub.isAlive);
                }
            }
        }
        /**
         * Notifies a subscription of this subscribable's current state.
         * @param sub The subscription to notify.
         */
        notifySubscription(sub) {
            sub.handler(this.get());
        }
        /**
         * Responds to when a subscription to this subscribable is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                if (this.singletonSub === sub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    const index = this.subs.indexOf(sub);
                    if (index >= 0) {
                        this.subs.splice(index, 1);
                    }
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        map(fn, equalityFunc, mutateFunc, initialVal) {
            return new MappedSubscribableClass(this, fn, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, mutateFunc, initialVal);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        pipe(to, arg2, arg3) {
            let sub;
            let paused;
            if (typeof arg2 === 'function') {
                sub = new SubscribablePipe(this, to, arg2, this.onSubDestroyedFunc);
                paused = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                sub = new SubscribablePipe(this, to, this.onSubDestroyedFunc);
                paused = arg2 !== null && arg2 !== void 0 ? arg2 : false;
            }
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
    }
    /**
     * Checks if two values are equal using the strict equality operator.
     * @param a The first value.
     * @param b The second value.
     * @returns whether a and b are equal.
     */
    AbstractSubscribable.DEFAULT_EQUALITY_FUNC = (a, b) => a === b;
    /**
     * An implementation of {@link MappedSubscribable}.
     */
    class MappedSubscribableClass extends AbstractSubscribable {
        /** @inheritdoc */
        get isAlive() {
            return this._isAlive;
        }
        /** @inheritdoc */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Constructor.
         * @param input This subscribable's input.
         * @param mapFunc The function which maps this subject's inputs to a value.
         * @param equalityFunc The function which this subject uses to check for equality between values.
         * @param mutateFunc The function which this subject uses to change its value.
         * @param initialVal The initial value of this subject.
         */
        constructor(input, mapFunc, equalityFunc, mutateFunc, initialVal) {
            super();
            this.input = input;
            this.mapFunc = mapFunc;
            this.equalityFunc = equalityFunc;
            this.canInitialNotify = true;
            this._isAlive = true;
            this._isPaused = false;
            if (initialVal && mutateFunc) {
                this.value = initialVal;
                mutateFunc(this.value, this.mapFunc(this.input.get()));
                this.mutateFunc = (newVal) => { mutateFunc(this.value, newVal); };
            }
            else {
                this.value = this.mapFunc(this.input.get());
                this.mutateFunc = (newVal) => { this.value = newVal; };
            }
            this.inputSub = this.input.sub(inputValue => {
                this.updateValue(inputValue);
            }, true);
        }
        /**
         * Re-maps this subject's value from its input, and notifies subscribers if this results in a change to the mapped
         * value according to this subject's equality function.
         * @param inputValue The input value.
         */
        updateValue(inputValue) {
            const value = this.mapFunc(inputValue, this.value);
            if (!this.equalityFunc(this.value, value)) {
                this.mutateFunc(value);
                this.notify();
            }
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        /** @inheritdoc */
        pause() {
            if (!this._isAlive) {
                throw new Error('MappedSubscribable: cannot pause a dead subscribable');
            }
            if (this._isPaused) {
                return this;
            }
            this.inputSub.pause();
            this._isPaused = true;
            return this;
        }
        /** @inheritdoc */
        resume() {
            if (!this._isAlive) {
                throw new Error('MappedSubscribable: cannot resume a dead subscribable');
            }
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            this.inputSub.resume(true);
            return this;
        }
        /** @inheritdoc */
        destroy() {
            this._isAlive = false;
            this.inputSub.destroy();
        }
    }

    /**
     * A subscribable subject which derives its value from an event consumer.
     */
    class ConsumerSubject extends AbstractSubscribable {
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether this subject is alive. While alive, this subject will update its value from its event consumer unless it
         * is paused. Once dead, this subject will no longer update its value and cannot be resumed again.
         */
        get isAlive() {
            return this._isAlive;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether event consumption is currently paused for this subject. While paused, this subject's value will not
         * update.
         */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Constructor.
         * @param consumer The event consumer from which this subject obtains its value. If null, this subject's value will
         * not be updated until its consumer is set to a non-null value.
         * @param initialVal This subject's initial value.
         * @param equalityFunc The function this subject uses check for equality between values.
         * @param mutateFunc The function this subject uses to change its value. If not defined, variable assignment is used
         * instead.
         */
        constructor(consumer, initialVal, equalityFunc, mutateFunc) {
            super();
            this.equalityFunc = equalityFunc;
            this.mutateFunc = mutateFunc;
            /** @inheritDoc */
            this.canInitialNotify = true;
            this.consumerHandler = this.onEventConsumed.bind(this);
            this.isValueConsumed = false;
            this.needSetDefaultValue = false;
            this._isAlive = true;
            this._isPaused = false;
            this.value = initialVal;
            this.consumerSub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(consumer, initialVal, equalityFunc, mutateFunc) {
            return new ConsumerSubject(consumer, initialVal, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, mutateFunc);
        }
        /**
         * Sets this subject's value and notifies subscribers if the value changed.
         * @param value The value to set.
         */
        setValue(value) {
            if (!this.equalityFunc(this.value, value)) {
                if (this.mutateFunc) {
                    this.mutateFunc(this.value, value);
                }
                else {
                    this.value = value;
                }
                this.notify();
            }
        }
        /**
         * Consumes an event.
         * @param value The value of the event.
         */
        onEventConsumed(value) {
            this.isValueConsumed = true;
            if (this.needSetDefaultValue) {
                this.needSetDefaultValue = false;
                delete this.defaultValue;
            }
            this.setValue(value);
        }
        /** @inheritDoc */
        get() {
            return this.value;
        }
        /**
         * Sets the consumer from which this subject derives its value. If the consumer is null, this subject's value will
         * not be updated until a non-null consumer is set.
         * @param consumer An event consumer.
         * @returns This subject, after its consumer has been set.
         */
        setConsumer(consumer) {
            var _a;
            if (!this._isAlive) {
                return this;
            }
            this.needSetDefaultValue = false;
            delete this.defaultValue;
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.consumerSub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler, this._isPaused);
            return this;
        }
        /**
         * Sets the consumer from which this subject derives its value and designates a default value to set if an event is
         * not immediately consumed from the new consumer when this subject is resumed. If the consumer is null, then this
         * subject's value will be set to the default value.
         * @param consumer An event consumer.
         * @param defaultVal The default value to set if the new consumer is null or if an event is not immediately consumed
         * from the new consumer when this subject is resumed.
         * @returns This subject, after its consumer has been set.
         */
        setConsumerWithDefault(consumer, defaultVal) {
            var _a;
            if (!this._isAlive) {
                return this;
            }
            this.defaultValue = defaultVal;
            this.needSetDefaultValue = true;
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.consumerSub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler, this._isPaused);
            if (!this._isPaused && this.needSetDefaultValue) {
                const defaultValue = this.defaultValue;
                this.needSetDefaultValue = false;
                delete this.defaultValue;
                this.setValue(defaultValue);
            }
            return this;
        }
        /**
         * Resets this subject to an initial value and optionally sets a new consumer from which this subject will derive its
         * value. If the consumer is null, then this subject's value will not be updated until a non-null consumer is set.
         *
         * The reset is treated as an atomic operation. If a non-null consumer is set and a consumed value immediately
         * replaces the initial value, then subscribers to this subject will only be notified of the change to the consumed
         * value instead of to both the change to the initial value and then to the consumed value.
         * @param initialVal The initial value to which to reset this subject.
         * @param consumer An event consumer. Defaults to `null`.
         * @returns This subject, after it has been reset.
         */
        reset(initialVal, consumer = null) {
            var _a;
            if (!this._isAlive) {
                return this;
            }
            this.isValueConsumed = false;
            this.needSetDefaultValue = false;
            delete this.defaultValue;
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.consumerSub = consumer === null || consumer === void 0 ? void 0 : consumer.handle(this.consumerHandler, this._isPaused);
            if (!this.isValueConsumed) {
                this.setValue(initialVal);
            }
            return this;
        }
        /**
         * Pauses consuming events for this subject. Once paused, this subject's value will not be updated.
         * @returns This subject, after it has been paused.
         */
        pause() {
            var _a;
            if (this._isPaused) {
                return this;
            }
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.pause();
            this._isPaused = true;
            return this;
        }
        /**
         * Resumes consuming events for this subject. Once resumed, this subject's value will be updated from consumed
         * events. When this subject is resumed, it immediately updates its value from its event consumer, if one exists.
         *
         * Any `initialNotify` argument passed to this method is ignored. This subject is always immediately notified of its
         * event consumer's value when resumed.
         * @returns This subject, after it has been resumed.
         */
        resume() {
            var _a;
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.resume(true);
            if (this.needSetDefaultValue) {
                const defaultValue = this.defaultValue;
                this.needSetDefaultValue = false;
                delete this.defaultValue;
                this.setValue(defaultValue);
            }
            return this;
        }
        /**
         * Destroys this subject. Once destroyed, it will no longer consume events to update its value.
         */
        destroy() {
            var _a;
            this._isAlive = false;
            (_a = this.consumerSub) === null || _a === void 0 ? void 0 : _a.destroy();
        }
    }

    /**
     * A publisher than handles publishing a debounced stall warning event based on an input AoA.
     */
    class StallWarningPublisher extends BasePublisher {
        /**
         * Creates an instance of the StallWarningPublisher. Requires the `aoa` (if not provided) and `on_ground` events from `AdcEvents``.
         * @param bus The event bus to use with this instance.
         * @param aoaThreshold The AoA stall warning threshold, where 1 is 100% of stall AoA.
         * @param debounceMs The amount of time, in milliseconds, to debounce the stall warning. Defaults to 500 ms.
         * @param aoa An optional subscribable that provides the AoA value, in degrees.
         */
        constructor(bus, aoaThreshold, debounceMs = 500, aoa) {
            super(bus);
            this.aoaThreshold = aoaThreshold;
            this.debounceMs = debounceMs;
            this.stallAoA = SimVar.GetSimVarValue('STALL ALPHA', exports.SimVarValueType.Degree);
            this.stallWarningOn = false;
            this.previousTime = -1;
            this.aoa = aoa !== null && aoa !== void 0 ? aoa : ConsumerSubject.create(bus.getSubscriber().on('aoa'), 0);
            this.onGround = ConsumerValue.create(bus.getSubscriber().on('on_ground'), true);
            this.debounceTimeRemaining = debounceMs;
        }
        /** @inheritdoc */
        onUpdate() {
            if (this.isPublishing()) {
                const time = Date.now();
                if (this.previousTime === -1) {
                    this.previousTime = time;
                }
                const deltaTime = time - this.previousTime;
                if (this.aoa.get() >= (this.aoaThreshold * this.stallAoA) && !this.onGround.get()) {
                    this.debounceTimeRemaining = Math.max(this.debounceTimeRemaining - deltaTime, 0);
                    if (this.debounceTimeRemaining === 0) {
                        this.setStallWarningOn(true);
                    }
                    else {
                        this.setStallWarningOn(false);
                    }
                }
                else {
                    this.debounceTimeRemaining = this.debounceMs;
                    this.setStallWarningOn(false);
                }
                this.previousTime = time;
            }
            else {
                this.previousTime = -1;
            }
        }
        /**
         * Sets whether the stall warning is on or not.
         * @param isOn Whether the stall warning is on.
         */
        setStallWarningOn(isOn) {
            if (this.stallWarningOn !== isOn) {
                this.publish('stall_warning_on', isOn, true, true);
                this.stallWarningOn = isOn;
            }
        }
    }

    /**
     * A publisher for control interactions.
     * This is meant to handle the events for which there aren't existing HEvents
     * in the sim to allow us to maintain a decoupled, event-driven architecture.
     */
    class ControlPublisher extends BasePublisher {
        /**
         * Create a ControlPublisher.
         * @param bus The EventBus to publish to.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer = undefined) {
            super(bus, pacer);
        }
        /**
         * Publish a control event.
         * @param event The event from ControlEvents.
         * @param value The value of the event.
         */
        publishEvent(event, value) {
            this.publish(event, value, true);
        }
        /** debug logger */
        startPublish() {
            super.startPublish();
            // console.log('control publisher started.');
        }
    }

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/utils/xmllogic" />
    /** The kind of data to return. */
    exports.CompositeLogicXMLValueType = void 0;
    (function (CompositeLogicXMLValueType) {
        CompositeLogicXMLValueType[CompositeLogicXMLValueType["Any"] = 0] = "Any";
        CompositeLogicXMLValueType[CompositeLogicXMLValueType["Number"] = 1] = "Number";
        CompositeLogicXMLValueType[CompositeLogicXMLValueType["String"] = 2] = "String";
    })(exports.CompositeLogicXMLValueType || (exports.CompositeLogicXMLValueType = {}));
    /**
     *
     */
    class CompositeLogicXMLHost {
        /**
         * Ctor
         * @param startPaused True to start paused.
         */
        constructor(startPaused = false) {
            this.anyHandlers = new Array();
            this.stringHandlers = new Array();
            this.numberHandlers = new Array();
            this.anyResultCache = new Array();
            this.stringResultCache = new Array();
            this.numberResultCache = new Array();
            this.context = new LogicXMLContext();
            this.isPaused = false;
            this.isPaused = startPaused;
        }
        /**
         * Set to pause the logic update loop.
         * @param isPaused True to pause, false to resume.
         */
        setIsPaused(isPaused) {
            this.isPaused = isPaused;
        }
        /**
         * Add a new logic element to calcluate a number or a string.
         * @param logic A CompositeLogicXMLElement.
         * @param handler A callback hander to take new values of either type.
         * @returns The current value of the logic.
         */
        addLogic(logic, handler) {
            this.anyHandlers.push({ logic: logic, handler: handler });
            return logic.getValue(this.context);
        }
        /**
         * Add a new logic element to calcluate a number.
         * @param logic A CompositeLogicXMLElement.
         * @param handler A callback hander to take new values as numbers.
         * @param precision An optional precision to require for updates to be sent.
         * @param smoothFactor An optional linear smoothing factor to apply to the value when updating.
         * @returns The current value of the logic.
         */
        addLogicAsNumber(logic, handler, precision, smoothFactor) {
            this.numberHandlers.push({ logic: logic, handler: handler, precision: precision, smoothFactor: smoothFactor });
            return logic.getValueAsNumber(this.context);
        }
        /**
         * Add a new logic element to calcluate a string.
         * @param logic A CompositeLogicXMLElement.
         * @param handler A callback hander to take new values as strings.
         * @returns The current value of the logic.
         */
        addLogicAsString(logic, handler) {
            this.stringHandlers.push({ logic: logic, handler: handler });
            return logic.getValueAsString(this.context);
        }
        /**
         * Add a function to the logic context.
         * @param funcSpec The XMLFunction configuration.
         * @returns The function's current value.
         */
        addFunction(funcSpec) {
            const func = new LogicXMLFunction();
            func.name = funcSpec.name;
            func.callback = funcSpec.logic;
            this.context.addFunction(func);
            return funcSpec.logic.getValue(this.context);
        }
        /**
         * Update every logic element and publish updates.
         * @param deltaTime The time since the last update, in ms.
         */
        update(deltaTime) {
            if (!this.isPaused) {
                for (let i = 0; i < this.anyHandlers.length; i++) {
                    const newVal = this.anyHandlers[i].logic.getValue(this.context);
                    if (newVal !== this.anyResultCache[i]) {
                        this.anyResultCache[i] = newVal;
                        this.anyHandlers[i].handler(newVal);
                    }
                }
                for (let i = 0; i < this.stringHandlers.length; i++) {
                    const newVal = this.stringHandlers[i].logic.getValueAsString(this.context);
                    if (newVal !== this.stringResultCache[i]) {
                        this.stringResultCache[i] = newVal;
                        this.stringHandlers[i].handler(newVal);
                    }
                }
                for (let i = 0; i < this.numberHandlers.length; i++) {
                    let newVal = this.numberHandlers[i].logic.getValueAsNumber(this.context);
                    let precision = this.numberHandlers[i].precision;
                    if (precision !== undefined) {
                        precision = Math.pow(10, precision);
                        newVal = Math.round(newVal * precision) / precision;
                    }
                    if (this.numberHandlers[i].smoothFactor !== undefined && this.numberHandlers[i].smoothFactor !== 0) {
                        // A smoothFactor of 0 means no smoothing.  We don't trigger this update if the factor is
                        // undefined or 0, but typescript still thinks is could be undefined due to the array indexing.
                        // The 'or-0' here is just to get around that without having to do a temporary assignment.
                        newVal = Utils.SmoothLinear(this.numberResultCache[i], newVal, this.numberHandlers[i].smoothFactor || 0, deltaTime);
                    }
                    if (newVal !== this.numberResultCache[i]) {
                        this.numberResultCache[i] = newVal;
                        this.numberHandlers[i].handler(newVal);
                    }
                }
                this.context.update();
            }
        }
    }

    /// <reference types="@microsoft/msfs-types/js/dataStorage" />
    /* eslint-disable no-inner-declarations */
    // eslint-disable-next-line @typescript-eslint/no-namespace
    exports.DataStore = void 0;
    (function (DataStore) {
        /**
         * Writes a keyed value to the data store.
         * @param key A key.
         * @param value The value to set.
         */
        function set(key, value) {
            SetStoredData(key, JSON.stringify(value));
        }
        DataStore.set = set;
        /**
         * Retrieves a keyed value from the data store.
         * @param key A key.
         * @returns the value stored under the key, or undefined if one could not be retrieved.
         */
        function get(key) {
            try {
                const string = GetStoredData(key);
                return JSON.parse(string);
            }
            catch (e) {
                return undefined;
            }
        }
        DataStore.get = get;
        /**
         * Removes a key from the data store.
         * @param key The key to remove.
         */
        function remove(key) {
            DeleteStoredData(key);
        }
        DataStore.remove = remove;
    })(exports.DataStore || (exports.DataStore = {}));

    /**
     * A subscribable subject whose value can be freely manipulated.
     */
    class Subject extends AbstractSubscribable {
        /**
         * Constructs an observable Subject.
         * @param value The initial value.
         * @param equalityFunc The function to use to check for equality.
         * @param mutateFunc The function to use to mutate the subject's value.
         */
        constructor(value, equalityFunc, mutateFunc) {
            super();
            this.value = value;
            this.equalityFunc = equalityFunc;
            this.mutateFunc = mutateFunc;
            this.isMutableSubscribable = true;
        }
        /**
         * Creates and returns a new Subject.
         * @param v The initial value of the subject.
         * @param equalityFunc The function to use to check for equality between subject values. Defaults to the strict
         * equality comparison (`===`).
         * @param mutateFunc The function to use to change the subject's value. If not defined, new values will replace
         * old values by variable assignment.
         * @returns A Subject instance.
         */
        static create(v, equalityFunc, mutateFunc) {
            return new Subject(v, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : Subject.DEFAULT_EQUALITY_FUNC, mutateFunc);
        }
        /** @inheritdoc */
        notifySub(sub) {
            sub(this.value);
        }
        /**
         * Sets the value of this subject and notifies subscribers if the value changed.
         * @param value The new value.
         */
        set(value) {
            if (!this.equalityFunc(value, this.value)) {
                if (this.mutateFunc) {
                    this.mutateFunc(this.value, value);
                }
                else {
                    this.value = value;
                }
                this.notify();
            }
        }
        /**
         * Applies a partial set of properties to this subject's value and notifies subscribers if the value changed as a
         * result.
         * @param value The properties to apply.
         */
        apply(value) {
            if (typeof this.value !== 'object' || this.value === null) {
                return;
            }
            let changed = false;
            for (const prop in value) {
                changed = value[prop] !== this.value[prop];
                if (changed) {
                    break;
                }
            }
            Object.assign(this.value, value);
            changed && this.notify();
        }
        /** @inheritdoc */
        notify() {
            super.notify();
        }
        /**
         * Gets the value of this subject.
         * @returns The value of this subject.
         */
        get() {
            return this.value;
        }
    }

    /**
     * A utility class which provides the current game state.
     */
    class GameStateProvider {
        /**
         * Constructor.
         */
        constructor() {
            this.gameState = Subject.create(undefined);
            window.document.addEventListener('OnVCockpitPanelAttributesChanged', this.onAttributesChanged.bind(this));
            this.onAttributesChanged();
        }
        /**
         * Responds to changes in document attributes.
         */
        onAttributesChanged() {
            var _a;
            if ((_a = window.parent) === null || _a === void 0 ? void 0 : _a.document.body.hasAttribute('gamestate')) {
                const attribute = window.parent.document.body.getAttribute('gamestate');
                if (attribute !== null) {
                    const state = GameState[attribute];
                    // The game state is set briefly to ingame after loading is finished before changing to briefing. In order to
                    // not notify subscribers of this erroneous ingame state, we will debounce any state changes into ingame by two
                    // frames.
                    if (state === GameState.ingame && this.gameState.get() !== GameState.ingame) {
                        setTimeout(() => {
                            setTimeout(() => {
                                const newAttribute = window.parent.document.body.getAttribute('gamestate');
                                if (newAttribute !== null) {
                                    this.gameState.set(GameState[newAttribute]);
                                }
                            });
                        });
                    }
                    else {
                        this.gameState.set(state);
                    }
                    return;
                }
            }
            this.gameState.set(undefined);
        }
        /**
         * Gets a subscribable which provides the current game state.
         * @returns A subscribable which provides the current game state.
         */
        static get() {
            var _a;
            return ((_a = GameStateProvider.INSTANCE) !== null && _a !== void 0 ? _a : (GameStateProvider.INSTANCE = new GameStateProvider())).gameState;
        }
    }

    /**
     * A manager for key events. Allows key events to be triggered and intercepted, and also publishes intercepted key
     * events on the event bus.
     */
    class KeyEventManager {
        /**
         * Constructor.
         * @param keyListener The Coherent key intercept view listener.
         * @param bus The event bus.
         */
        constructor(keyListener, bus) {
            this.keyListener = keyListener;
            this.bus = bus;
            Coherent.on('keyIntercepted', this.onKeyIntercepted.bind(this));
        }
        /**
         * Responds to key intercept events.
         * @param key The key that was intercepted.
         * @param value1 The second data value of the key event.
         * @param value0 The first data value of the key event.
         * @param value2 The third data value of the key event.
         */
        onKeyIntercepted(key, value1, value0, value2) {
            // Even though values are uint32, we will do what the sim does and pretend they're actually sint32
            if (value0 !== undefined && value0 >= 2147483648) {
                value0 -= 4294967296;
            }
            this.bus.pub('key_intercept', { key, value0, value1, value2 }, false, false);
        }
        /**
         * Triggers a key event.
         * @param key The key to trigger.
         * @param bypass Whether the event should bypass intercepts.
         * @param value0 The first data value of the key event. Defaults to `0`.
         * @param value1 The second data value of the key event. Defaults to `0`.
         * @param value2 The third data value of the key event. Defaults to `0`.
         * @returns A Promise which is fulfilled after the key event has been triggered.
         */
        triggerKey(key, bypass, value0 = 0, value1 = 0, value2 = 0) {
            return Coherent.call('TRIGGER_KEY_EVENT', key, bypass, value0, value1, value2);
        }
        /**
         * Enables interception for a key.
         * @param key The key to intercept.
         * @param passThrough Whether to pass the event through to the sim after it has been intercepted.
         */
        interceptKey(key, passThrough) {
            Coherent.call('INTERCEPT_KEY_EVENT', key, passThrough ? 0 : 1);
        }
        /**
         * Gets an instance of KeyEventManager. If an instance does not already exist, a new one will be created.
         * @param bus The event bus.
         * @returns A Promise which will be fulfilled with an instance of KeyEventManager.
         */
        static getManager(bus) {
            if (KeyEventManager.INSTANCE) {
                return Promise.resolve(KeyEventManager.INSTANCE);
            }
            if (!KeyEventManager.isCreatingInstance) {
                KeyEventManager.createInstance(bus);
            }
            return new Promise(resolve => {
                KeyEventManager.pendingPromiseResolves.push(resolve);
            });
        }
        /**
         * Creates an instance of KeyEventManager and fulfills all pending Promises to get the manager instance once
         * the instance is created.
         * @param bus The event bus.
         */
        static async createInstance(bus) {
            KeyEventManager.isCreatingInstance = true;
            KeyEventManager.INSTANCE = await KeyEventManager.create(bus);
            KeyEventManager.isCreatingInstance = false;
            for (let i = 0; i < KeyEventManager.pendingPromiseResolves.length; i++) {
                KeyEventManager.pendingPromiseResolves[i](KeyEventManager.INSTANCE);
            }
        }
        /**
         * Creates an instance of KeyEventManager.
         * @param bus The event bus.
         * @returns A Promise which is fulfilled with a new instance of KeyEventManager after it has been created.
         */
        static create(bus) {
            return new Promise((resolve, reject) => {
                const gameState = GameStateProvider.get();
                const sub = gameState.sub(state => {
                    if (window['IsDestroying']) {
                        sub.destroy();
                        reject('KeyEventManager: cannot create a key intercept manager after the Coherent JS view has been destroyed');
                        return;
                    }
                    if (state === GameState.briefing || state === GameState.ingame) {
                        sub.destroy();
                        const keyListener = RegisterViewListener('JS_LISTENER_KEYEVENT', () => {
                            if (window['IsDestroying']) {
                                reject('KeyEventManager: cannot create a key intercept manager after the Coherent JS view has been destroyed');
                                return;
                            }
                            resolve(new KeyEventManager(keyListener, bus));
                        });
                    }
                }, false, true);
                sub.resume(true);
            });
        }
    }
    KeyEventManager.isCreatingInstance = false;
    KeyEventManager.pendingPromiseResolves = [];

    /**
     * A publisher for Accelerometer information.
     */
    class AccelerometerPublisher extends SimVarPublisher {
        /**
         * Creates an AccelerometerPublisher.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            const simvars = new Map([
                ['g_force', { name: 'G FORCE', type: exports.SimVarValueType.Number }],
                ['max_g_force', { name: 'MAX G FORCE', type: exports.SimVarValueType.Number }],
                ['min_g_force', { name: 'MIN G FORCE', type: exports.SimVarValueType.Number }],
            ]);
            super(simvars, bus, pacer);
        }
    }

    /**
     * A utility class for working with common aeronautical constants and calculations.
     */
    class AeroMath {
        // ---- Ideal gas law relationships for air ----
        /**
         * Gets the static pressure of air, in hectopascals, given temperature and density.
         * @param temperature The temperature, in degrees Celsius.
         * @param density The density, in kilograms per cubic meter.
         * @returns The static pressure of air, in hectopascals, with the specified temperature and density.
         */
        static pressureAir(temperature, density) {
            return density * AeroMath.R_AIR * (temperature + 273.15) / 100;
        }
        /**
         * Gets the density of air, in kilograms per cubic meter, given static pressure and temperature.
         * @param pressure The static pressure, in hectopascals.
         * @param temperature The temperature, in degrees Celsius.
         * @returns The density of air, in kilograms per cubic meter, with the specified static pressure and temperature.
         */
        static densityAir(pressure, temperature) {
            return pressure * 100 / (AeroMath.R_AIR * (temperature + 273.15));
        }
        /**
         * Gets the temperature of air, in degrees Celsius, given static pressure and density.
         * @param pressure The static pressure, in hectopascals.
         * @param density The density, in kilograms per cubic meter.
         * @returns The temperature of air, in degrees Celsius, with the specified static pressure and temperature.
         */
        static temperatureAir(pressure, density) {
            return pressure * 100 / (AeroMath.R_AIR * density) - 273.15;
        }
        // ---- Other properties of air ----.
        /**
         * Gets the speed of sound in air, in meters per second, for a given temperature.
         * @param temperature The temperature, in degrees Celsius.
         * @returns The speed of sound in air, in meters per second, for the given temperature.
         */
        static soundSpeedAir(temperature) {
            // speed of sound = sqrt(gamma * R * T)
            // gamma = 1.4
            // R = specific gas constant of dry air
            return Math.sqrt(401.8798068394 * (temperature + 273.15));
        }
        // ---- Pressure ratios ----
        /**
         * Gets the ratio of total pressure to static pressure for a given mach number in a subsonic compressible airflow.
         * @param mach The mach number.
         * @returns The ratio of total pressure to static pressure for the specific mach number.
         */
        static totalPressureRatioAir(mach) {
            return Math.pow(1 + 0.2 * mach * mach, 3.5);
        }
        // ---- Temperature ratios ----
        /**
         * Gets the ratio of total air temperature to static air temperature for a given mach number.
         * @param mach The mach number.
         * @param recovery The recovery factor. This is a value in the range `[0, 1]` representing the fraction of the
         * kinetic energy of the airflow that is converted to heat. Defaults to 1.
         * @returns The ratio of total air temperature to static air temperature for the specified mach number.
         */
        static totalTemperatureRatioAir(mach, recovery = 1) {
            return 1 + 0.2 * recovery * mach * mach;
        }
        // ---- ISA modeling ----
        /**
         * Gets the ISA temperature, in degrees Celsius, at a given pressure altitude. The supported pressure altitude range
         * is from -610 to 80000 meters above MSL. This method will return the temperature at -610 meters for all altitudes
         * below this range, and the temperature at 80000 meters for all altitudes above this range.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The ISA temperature, in degrees Celsius, for the specified pressure altitude.
         */
        static isaTemperature(altitude) {
            // We don't use lookup table for perf reasons.
            if (altitude < 11000) {
                return 15 + Math.max(altitude, -610) * -0.0065;
            }
            else if (altitude < 20000) {
                return -56.5;
            }
            else if (altitude < 32000) {
                return -56.5 + (altitude - 20000) * 0.001;
            }
            else if (altitude < 47000) {
                return -44.5 + (altitude - 32000) * 0.0028;
            }
            else if (altitude < 51000) {
                return -2.5;
            }
            else if (altitude < 71000) {
                return -2.5 + (altitude - 51000) * -0.0028;
            }
            else {
                return -58.5 + (Math.min(altitude, 80000) - 71000) * -0.002;
            }
        }
        /**
         * Gets the ISA pressure, in hectopascals, at a given pressure altitude. The supported pressure altitude range is
         * from -610 to 80000 meters above MSL. This method will return the pressure at -610 meters for all altitudes below
         * this range, and the pressure at 80000 meters for all altitudes above this range.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The ISA pressure, in hectopascals, for the specified pressure altitude.
         */
        static isaPressure(altitude) {
            // ISA pressure modeling uses the following equation:
            // dP/dh = -density/g
            // Using the ideal gas law to substitute density with temperature and solving the above DE generates two
            // different equations depending on whether temperature is constant with respect to altitude:
            // Temperature varies with altitude:
            // P(h) = P(h0) * (1 + dT/dh / T(h0) * (h - h0)) ^ (-g / (R * dT/dh))
            // Temperature constant with altitude:
            // P(h) = P(h0) * e^(-g / (R * T) * (h - h0))
            // g = gravitational acceleration
            // R = specific gas constant of dry air
            if (altitude < -610) {
                // Modeling stops at -610 meters, so return the pressure for -610 meters for any altitude below this.
                return 1088.707021458965;
            }
            else if (altitude <= 11000) {
                // Troposphere
                // dT/dh = -0.0065 kelvin per meter
                return 1013.25 * Math.pow(1 - 2.25577e-5 * altitude, 5.25580);
            }
            else if (altitude <= 20000) {
                // Tropopause
                // dT/dh = 0
                return 226.32547681422847 * Math.exp(-1.57686e-4 * (altitude - 11000));
            }
            else if (altitude <= 32000) {
                // Lower stratosphere
                // dT/dh = 0.001 kelvin per meter
                return 54.7512459834976 * Math.pow(1 + 4.61574e-6 * (altitude - 20000), -34.1627);
            }
            else if (altitude <= 47000) {
                // Upper stratosphere
                // dT/dh = 0.0028 kelvin per meter
                return 8.68079131804552 * Math.pow(1 + 1.22458e-5 * (altitude - 32000), -12.2010);
            }
            else if (altitude <= 51000) {
                // Stratopause
                // dT/dh = 0
                return 1.1091650294132658 * Math.exp(-1.26225e-4 * (altitude - 47000));
            }
            else if (altitude <= 71000) {
                // Lower mesosphere
                // dT/dh = -0.0028 kelvin per meter
                return 0.6694542213945832 * Math.pow(1 - 1.03455e-5 * (altitude - 51000), 12.2010);
            }
            else if (altitude <= 80000) {
                // Upper mesosphere
                // dT/dh = -0.002 kelvin per meter
                return 0.03956893750841349 * Math.pow(1 - 9.31749e-6 * (altitude - 71000), 17.0814);
            }
            else {
                // Modeling stops at 80000 meters, so return the pressure for 80000 meters for any altitude above this.
                return 0.008864013902895545;
            }
        }
        /**
         * Gets the pressure altitude, in meters above MSL, corresponding to a given ISA pressure. The supported pressure
         * altitude range is from -610 to 80000 meters above MSL. This method will return -610 meters for all pressures above
         * the pressure at -610 meters, and 80000 meters for all pressures below the pressure at 80000 meters.
         * @param pressure The ISA pressure for which to get the altitude, in hectopascals.
         * @returns The pressure altitude, in meters above MSL, corresponding to the specified ISA pressure.
         */
        static isaAltitude(pressure) {
            // ISA pressure modeling uses the following equation:
            // dP/dh = -density/g
            // Using the ideal gas law to substitute density with temperature and solving the above DE generates two
            // different equations depending on whether temperature is constant with respect to altitude:
            // Temperature varies with altitude:
            // h(P) = T(h0) / dT/dh * ((P / P(h0)) ^ (-(R * dT/dh) / g) - 1) + h0
            // Temperature constant with altitude:
            // h(P) = -(R * T) / g * ln(P / P(h0)) + h0
            // g = gravitational acceleration
            // R = specific gas constant of dry air
            if (pressure > 1088.707021458965) {
                // Modeling stops at -610 meters, so return -610 meters for any pressure above the pressure at this altitude.
                return -610;
            }
            else if (pressure > 226.32547681422847) {
                // Troposphere
                // dT/dh = -0.0065 kelvin per meter
                return -44330.76067152236 * (Math.pow(pressure / 1013.25, 0.1902659918566155) - 1);
            }
            else if (pressure > 54.7512459834976) {
                // Tropopause
                // dT/dh = 0
                return -6341.717083317479 * Math.log(pressure / 226.32547681422847) + 11000;
            }
            else if (pressure > 8.68079131804552) {
                // Lower stratosphere
                // dT/dh = 0.001 kelvin per meter
                return 216649.984617851092 * (Math.pow(pressure / 54.7512459834976, -0.02927169105486393) - 1) + 20000;
            }
            else if (pressure > 1.1091650294132658) {
                // Upper stratosphere
                // dT/dh = 0.0028 kelvin per meter
                return 81660.6509987098 * (Math.pow(pressure / 8.68079131804552, -0.08196049504139005) - 1) + 32000;
            }
            else if (pressure > 0.6694542213945832) {
                // Stratopause
                // dT/dh = 0
                return -7922.360863537334 * Math.log(pressure / 1.1091650294132658) + 47000;
            }
            else if (pressure > 0.03956893750841349) {
                // Lower mesosphere
                // dT/dh = -0.0028 kelvin per meter
                return -96660.38374172345 * (Math.pow(pressure / 0.6694542213945832, 0.08196049504139005) - 1) + 51000;
            }
            else if (pressure > 0.008864013902895545) {
                // Upper mesosphere
                // dT/dh = -0.002 kelvin per meter
                return -107325.0414006347 * (Math.pow(pressure / 0.03956893750841349, 0.05854321074385004) - 1) + 71000;
            }
            else {
                // Modeling stops at 80000 meters, so return 80000 meters for any pressure below the pressure at this altitude.
                return 80000;
            }
        }
        /**
         * Gets the ISA density, in kilograms per cubic meter, at a given pressure altitude. The supported pressure altitude
         * range is from -610 to 80000 meters above MSL. This method will return the density at -610 meters for all altitudes
         * below this range, and the density at 80000 meters for all altitudes above this range.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The ISA density, in kilograms per cubic meter, for the specified pressure altitude.
         */
        static isaDensity(altitude, deltaIsa = 0) {
            return AeroMath.densityAir(AeroMath.isaPressure(altitude), AeroMath.isaTemperature(altitude) + deltaIsa);
        }
        /**
         * Gets the speed of sound, in meters per second, at a given pressure altitude under ISA conditions.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The speed of sound, in meters per second, at the specified pressure altitude under ISA conditions.
         */
        static soundSpeedIsa(altitude, deltaIsa = 0) {
            return this.soundSpeedAir(AeroMath.isaTemperature(altitude) + deltaIsa);
        }
        /**
         * Gets the offset to apply to pressure altitude, in meters, to obtain indicated altitude for a given altimeter
         * barometric pressure setting.
         * @param baro The barometic pressure setting, in hectopascals, for which to get the offset.
         * @returns The offset to apply to pressure altitude, in meters, to obtain indicated altitude for the specified
         * altimeter barometric pressure setting.
         */
        static baroPressureAltitudeOffset(baro) {
            return 44330.76067152236 * (Math.pow(baro / 1013.25, 0.1902659918566155) - 1);
        }
        /**
         * Gets the altimeter barometric pressure setting, in hectopascals, to obtain a given offset to apply to pressure
         * altitude to yield indicated altitude.
         * @param offset The altitude offset, in meters, for which to get the barometric pressure setting.
         * @returns The altimeter barometric pressure setting, in hectopascals, to obtain the specified offset to apply to
         * pressure altitude to yield indicated altitude.
         */
        static altitudeOffsetBaroPressure(offset) {
            return 1013.25 * Math.pow(1 + 2.25577e-5 * offset, 5.25580);
        }
        // ---- Speed conversions ----
        // The following section contains methods for converting between different speeds: CAS, TAS, EAS, and mach.
        // All conversions are based on the following:
        // Constants:
        // gamma (adiabatic index of air) = 1.4
        // pressure_sea_level_isa = 1013.25 hPa
        // sound_speed_sea_level_isa = 340.2964 m/s
        // density_sea_level_isa = 1.22498 kg/m^3
        // Relationship between mach and impact pressure (only valid for subsonic flow):
        // mach = sqrt(5 * ((impact_pressure / static_pressure + 1) ^ ((gamma - 1) / gamma)) - 1)
        // impact_pressure = static_pressure * ((1 + 0.2 * mach ^ 2) ^ (gamma / (gamma - 1)) - 1)
        // Relationship between mach and airspeed:
        // mach = airspeed / sound_speed
        // airspeed = mach * sound_speed
        // Relationship between EAS and TAS:
        // eas = tas * sqrt(density / density_sea_level_isa)
        /**
         * Converts true airspeed (TAS) to mach number.
         * @param tas The true airspeed to convert, in the same units as `soundSpeed`.
         * @param soundSpeed The speed of sound, in the same units as `tas`.
         * @returns The mach number equivalent of the specified true airspeed.
         */
        static tasToMach(tas, soundSpeed) {
            return tas / soundSpeed;
        }
        /**
         * Converts true airspeed (TAS) to mach number under ISA conditions.
         * @param tas The true airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The mach number equivalent of the specified true airspeed at the specified pressure altitude under ISA
         * conditions.
         */
        static tasToMachIsa(tas, altitude, deltaIsa = 0) {
            return tas / AeroMath.soundSpeedIsa(altitude, deltaIsa);
        }
        /**
         * Converts mach number to true airspeed (TAS).
         * @param mach The mach number to convert.
         * @param soundSpeed The speed of sound.
         * @returns The true airspeed equivalent of the specified mach number, in the same units as `soundSpeed`.
         */
        static machToTas(mach, soundSpeed) {
            return mach * soundSpeed;
        }
        /**
         * Converts mach number to true airspeed (TAS), in meters per second, under ISA conditions.
         * @param mach The mach number to convert.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The true airspeed equivalent, in meters per second, of the specified mach number at the specified
         * pressure altitude under ISA conditions.
         */
        static machToTasIsa(mach, altitude, deltaIsa = 0) {
            return mach * AeroMath.soundSpeedIsa(altitude, deltaIsa);
        }
        /**
         * Converts calibrated airspeed (CAS) to mach number. The conversion is only valid for subsonic speeds.
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The mach number equivalent of the specified calibrated airspeed at the specified static pressure.
         */
        static casToMach(cas, pressure) {
            // Calibrated airspeed is effectively the airspeed at sea level under ISA conditions that would produce the same
            // impact pressure as the airplane's observed impact pressure. Therefore, we can calculate mach from CAS by
            // calculating the impact pressure using sea level ISA conditions, then using that impact pressure to calculate
            // mach using ambient static pressure.
            const mach0 = cas / AeroMath.SOUND_SPEED_SEA_LEVEL_ISA;
            const impactPressure = 1013.25 * (Math.pow(1 + (0.2 * mach0 * mach0), 3.5) - 1);
            return Math.sqrt(5 * (Math.pow(impactPressure / pressure + 1, 2 / 7) - 1));
        }
        /**
         * Converts calibrated airspeed (CAS) to mach number under ISA conditions. The conversion is only valid for subsonic
         * speeds.
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The mach number equivalent of the specified calibrated airspeed at the specified pressure altitude under
         * ISA conditions.
         */
        static casToMachIsa(cas, altitude) {
            return AeroMath.casToMach(cas, AeroMath.isaPressure(altitude));
        }
        /**
         * Converts mach number to calibrated airspeed (CAS). The conversion is only valid for subsonic speeds.
         * @param mach The mach number to convert.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The calibrated airspeed equivalent in meters per second of the specified mach number at the specified
         * static pressure.
         */
        static machToCas(mach, pressure) {
            // Calibrated airspeed is effectively the airspeed at sea level under ISA conditions that would produce the same
            // impact pressure as the airplane's observed impact pressure. Therefore, we can calculate CAS from mach by
            // calculating the impact pressure using ambient static pressure, then using that impact pressure to calculate
            // CAS using sea level ISA conditions.
            const impactPressure = pressure * (Math.pow(1 + 0.2 * mach * mach, 3.5) - 1);
            return AeroMath.SOUND_SPEED_SEA_LEVEL_ISA * Math.sqrt(5 * (Math.pow(impactPressure / 1013.25 + 1, 2 / 7) - 1));
        }
        /**
         * Converts mach number to calibrated airspeed (CAS) under ISA conditions. The conversion is only valid for subsonic
         * speeds.
         * @param mach The mach number to convert.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The calibrated airspeed equivalent in meters per second of the specified mach number  at the specified
         * pressure altitude under ISA conditions.
         */
        static machToCasIsa(mach, altitude) {
            return AeroMath.machToCas(mach, AeroMath.isaPressure(altitude));
        }
        /**
         * Converts calibrated airspeed (CAS) to true airspeed (TAS).
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @param temperature The ambient static temperature, in degrees Celsius.
         * @returns The true airspeed equivalent, in meters per second, of the specified calibrated airspeed at the specified
         * ambient pressure and temperature.
         */
        static casToTas(cas, pressure, temperature) {
            return AeroMath.casToMach(cas, pressure) * AeroMath.soundSpeedAir(temperature);
        }
        /**
         * Converts calibrated airspeed (CAS) to true airspeed (TAS) under ISA conditions.
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The true airspeed equivalent, in meters per second, of the specified calibrated airspeed at the specified
         * pressure altitude under ISA conditions.
         */
        static casToTasIsa(cas, altitude, deltaIsa = 0) {
            return AeroMath.casToMachIsa(cas, altitude) * AeroMath.soundSpeedIsa(altitude, deltaIsa);
        }
        /**
         * Converts true airspeed (TAS) to calibrated airspeed (CAS).
         * @param tas The true airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @param temperature The ambient static temperature, in degrees Celsius.
         * @returns The calibrated airspeed equivalent, in meters per second, of the specified true airspeed at the specified
         * ambient pressure and temperature.
         */
        static tasToCas(tas, pressure, temperature) {
            return AeroMath.machToCas(tas / AeroMath.soundSpeedAir(temperature), pressure);
        }
        /**
         * Converts true airspeed (TAS) to calibrated airspeed (CAS) under ISA conditions.
         * @param tas The true airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The calibrated airspeed equivalent, in meters per second, of the specified true airspeed at the specified
         * pressure altitude under ISA conditions.
         */
        static tasToCasIsa(tas, altitude, deltaIsa = 0) {
            return AeroMath.machToCasIsa(tas / AeroMath.soundSpeedIsa(altitude, deltaIsa), altitude);
        }
        /**
         * Converts true airspeed (TAS) to equivalent airspeed (EAS).
         * @param tas The true airspeed to convert.
         * @param density The ambient density, in kilograms per cubic meter.
         * @returns The equivalent airspeed corresponding to the specified true airspeed at the specified ambient density.
         * The equivalent airspeed is expressed in the same units as the true airspeed.
         */
        static tasToEas(tas, density) {
            return tas * Math.sqrt(density / AeroMath.DENSITY_SEA_LEVEL_ISA);
        }
        /**
         * Converts true airspeed (TAS) to equivalent airspeed (EAS) under ISA conditions.
         * @param tas The true airspeed to convert.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The equivalent airspeed corresponding to the specified true airspeed at the specified pressure altitude
         * under ISA conditions. The equivalent airspeed is expressed in the same units as the true airspeed.
         */
        static tasToEasIsa(tas, altitude, deltaIsa = 0) {
            return AeroMath.tasToEas(tas, AeroMath.isaDensity(altitude, deltaIsa));
        }
        /**
         * Converts equivalent airspeed (EAS) to true airspeed (TAS).
         * @param eas The equivalent airspeed to convert.
         * @param density The ambient density, in kilograms per cubic meter.
         * @returns The true airspeed corresponding to the specified equivalent airspeed at the specified ambient density.
         * The true airspeed is expressed in the same units as the equivalent airspeed.
         */
        static easToTas(eas, density) {
            return eas * Math.sqrt(AeroMath.DENSITY_SEA_LEVEL_ISA / density);
        }
        /**
         * Converts equivalent airspeed (EAS) to true airspeed (TAS) under ISA conditions.
         * @param eas The equivalent airspeed to convert.
         * @param altitude The pressure altitude, in meters above MSL.
         * @param deltaIsa The deviation from ISA temperature, in degrees Celsius. Defaults to `0`.
         * @returns The true airspeed corresponding to the specified equivalent airspeed at the specified pressure altitude
         * under ISA conditions. The true airspeed is expressed in the same units as the equivalent airspeed.
         */
        static easToTasIsa(eas, altitude, deltaIsa = 0) {
            return AeroMath.easToTas(eas, AeroMath.isaDensity(altitude, deltaIsa));
        }
        /**
         * Converts mach number to equivalent airspeed (EAS).
         * @param mach The mach number to convert.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The equivalent airspeed, in meters per second, corresponding to the specified mach number at the
         * specified ambient static pressure.
         */
        static machToEas(mach, pressure) {
            return AeroMath.SOUND_SPEED_SEA_LEVEL_ISA * mach * Math.sqrt(pressure / 1013.25);
        }
        /**
         * Converts mach number to equivalent airspeed (EAS) under ISA conditions.
         * @param mach The mach number to convert.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The equivalent airspeed, in meters per second, corresponding to the specified mach number at the
         * specified pressure altitude under ISA conditions.
         */
        static machToEasIsa(mach, altitude) {
            return AeroMath.machToEas(mach, AeroMath.isaPressure(altitude));
        }
        /**
         * Converts equivalent airspeed (EAS) to mach number.
         * @param eas The equivalent airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The mach number corresponding to the specified equivalent airspeed at the specified ambient static
         * pressure.
         */
        static easToMach(eas, pressure) {
            return eas * Math.sqrt(1013.25 / pressure) / AeroMath.SOUND_SPEED_SEA_LEVEL_ISA;
        }
        /**
         * Converts equivalent airspeed (EAS) to mach number under ISA conditions.
         * @param eas The equivalent airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The mach number corresponding to the specified equivalent airspeed at the specified pressure altitude
         * under ISA conditions.
         */
        static easToMachIsa(eas, altitude) {
            return AeroMath.easToMach(eas, AeroMath.isaPressure(altitude));
        }
        /**
         * Converts calibrated airspeed (CAS) to equivalent airspeed (EAS). The conversion is only valid for subsonic speeds.
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The equivalent airspeed, in meters per second, corresponding to the specified calibrated airspeed at the
         * specified ambient static pressure.
         */
        static casToEas(cas, pressure) {
            // The below is a slightly optimized concatenation of the CAS-to-mach and mach-to-EAS conversions.
            const mach0 = cas / AeroMath.SOUND_SPEED_SEA_LEVEL_ISA;
            const impactPressure = 1013.25 * (Math.pow(1 + (0.2 * mach0 * mach0), 3.5) - 1);
            return AeroMath.SOUND_SPEED_SEA_LEVEL_ISA * Math.sqrt(5 * pressure / 1013.25 * (Math.pow(impactPressure / pressure + 1, 2 / 7) - 1));
        }
        /**
         * Converts calibrated airspeed (CAS) to equivalent airspeed (EAS) under ISA conditions. The conversion is only valid
         * for subsonic speeds.
         * @param cas The calibrated airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The equivalent airspeed, in meters per second, corresponding to the specified calibrated airspeed at the
         * specified pressure altitude under ISA conditions.
         */
        static casToEasIsa(cas, altitude) {
            return AeroMath.casToEas(cas, AeroMath.isaPressure(altitude));
        }
        /**
         * Converts equivalent airspeed (EAS) to calibrated airspeed (CAS). The conversion is only valid for subsonic speeds.
         * @param eas The equivalent airspeed to convert, in meters per second.
         * @param pressure The ambient static pressure, in hectopascals.
         * @returns The calibrated airspeed, in meters per second, corresponding to the specified equivalent airspeed at the
         * specified ambient static pressure.
         */
        static easToCas(eas, pressure) {
            return AeroMath.machToCas(AeroMath.easToMach(eas, pressure), pressure);
        }
        /**
         * Converts equivalent airspeed (EAS) to calibrated airspeed (CAS) under ISA conditions. The conversion is only valid
         * for subsonic speeds.
         * @param eas The equivalent airspeed to convert, in meters per second.
         * @param altitude The pressure altitude, in meters above MSL.
         * @returns The calibrated airspeed, in meters per second, corresponding to the specified equivalent airspeed at the
         * specified pressure altitude under ISA conditions.
         */
        static easToCasIsa(eas, altitude) {
            return AeroMath.easToCas(eas, AeroMath.isaPressure(altitude));
        }
        // ---- Lift and drag equations ----
        // force_coefficient = force / (dynamic_pressure * area)
        // dynamic_pressure = 0.5 * density * speed ^ 2
        /**
         * Calculates a fluid flow force coefficient given a force and flow parameters.
         * @param force The flow force, in newtons.
         * @param area The reference area, in meters squared.
         * @param arg3 The flow dynamic pressure, in hectopascals, if `arg4` is not defined; otherwise the flow density, in
         * kilograms per cubic meter.
         * @param arg4 The flow speed, in meters per second.
         * @returns The fluid flow force coefficient given the specified force and flow parameters.
         */
        static flowCoefFromForce(force, area, arg3, arg4) {
            const dynamicPressure = arg4 === undefined ? arg3 * 100 : 0.5 * arg3 * arg4 * arg4;
            return force / (dynamicPressure * area);
        }
        /**
         * Calculates a fluid flow force given a coefficient and flow parameters.
         * @param coef The flow force coefficient.
         * @param area The reference area, in meters squared.
         * @param arg3 The flow dynamic pressure, in hectopascals, if `arg4` is not defined; otherwise the flow density, in
         * kilograms per cubic meter.
         * @param arg4 The flow speed, in meters per second.
         * @returns The fluid flow force given the specified coefficient and flow parameters.
         */
        static flowForceFromCoef(coef, area, arg3, arg4) {
            const dynamicPressure = arg4 === undefined ? arg3 * 100 : 0.5 * arg3 * arg4 * arg4;
            return coef * dynamicPressure * area;
        }
        // ---- Engine equations ----
        /**
         * Calculates a thrust correction factor. Multiplying the correction factor by uncorrected thrust yields corrected
         * thrust.
         * @param mach The mach number.
         * @param ambientPressure The ambient static pressure in hPa.
         * @returns The thrust correction factor for the specified parameters.
         */
        static thrustCorrectionFactor(mach, ambientPressure) {
            const totalInletPressureMachRise = AeroMath.totalPressureRatioAir(mach);
            const deltaPressureRatio = ambientPressure / 1013.25;
            return 1 / (deltaPressureRatio * totalInletPressureMachRise);
        }
    }
    /** The ideal gas constant, in units of joules per mole per kelvin. */
    AeroMath.R = 8.314462618153;
    /** The specific gas constant of dry air, in units of joules per kilogram per kelvin. */
    AeroMath.R_AIR = 287.057;
    /** Approximate value of the adiabatic index of air near room temperature. */
    AeroMath.GAMMA_AIR = 1.4;
    /** The speed of sound in air at sea level under ISA conditions, in meters per second. */
    AeroMath.SOUND_SPEED_SEA_LEVEL_ISA = 340.2964;
    /** The density of air at sea level under ISA conditions, in kilograms per cubic meter. */
    AeroMath.DENSITY_SEA_LEVEL_ISA = AeroMath.isaDensity(0);
    AeroMath.liftCoefficient = AeroMath.flowCoefFromForce;
    AeroMath.lift = AeroMath.flowForceFromCoef;
    AeroMath.dragCoefficient = AeroMath.flowCoefFromForce;
    AeroMath.drag = AeroMath.flowForceFromCoef;

    /**
     * A number with an associated unit. Each NumberUnit is created with a reference unit type,
     * which cannot be changed after instantiation. The reference unit type determines how the
     * value of the NumberUnit is internally represented. Each NumberUnit also maintains an
     * active unit type, which can be dynamically changed at any time.
     */
    class NumberUnit {
        /**
         * Constructor.
         * @param number - the initial numeric value of the new NumberUnit.
         * @param unit - the unit type of the new NumberUnit.
         */
        constructor(number, unit) {
            this._number = number;
            this._unit = unit;
            this.readonly = new NumberUnitReadOnly(this);
        }
        /**
         * Gets this NumberUnit's numeric value.
         * @returns This NumberUnit's numeric value.
         */
        get number() {
            return this._number;
        }
        /**
         * Gets this NumberUnit's unit type.
         * @returns This NumberUnit's unit type.
         */
        get unit() {
            return this._unit;
        }
        /**
         * Converts a value to a numeric value with this NumberUnit's unit type.
         * @param value - the value.
         * @param unit - the unit type of the new value. Defaults to this NumberUnit's unit type. This argument is ignored if
         * value is a NumberUnit.
         * @returns the numeric of the value with this NumberUnit's unit type.
         */
        toNumberOfThisUnit(value, unit) {
            if ((typeof value !== 'number') && this.unit.canConvert(value.unit)) {
                return this.unit.convertFrom(value.number, value.unit);
            }
            if (typeof value === 'number' && (!unit || this.unit.canConvert(unit))) {
                return unit ? this.unit.convertFrom(value, unit) : value;
            }
            return undefined;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2) {
            const converted = this.toNumberOfThisUnit(arg1, arg2);
            if (converted !== undefined) {
                this._number = converted;
                return this;
            }
            throw new Error('Invalid unit conversion attempted.');
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        add(arg1, arg2, arg3) {
            const isArg2NumberUnit = arg2 instanceof NumberUnit;
            const converted = this.toNumberOfThisUnit(arg1, isArg2NumberUnit ? undefined : arg2);
            if (converted !== undefined) {
                let out = isArg2NumberUnit ? arg2 : arg3;
                if (out) {
                    out.set(this.number + converted, this.unit);
                }
                else {
                    // eslint-disable-next-line @typescript-eslint/no-this-alias
                    out = this;
                    this._number += converted;
                }
                return out;
            }
            throw new Error('Invalid unit conversion attempted.');
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        subtract(arg1, arg2, arg3) {
            const isArg2NumberUnit = arg2 instanceof NumberUnit;
            const converted = this.toNumberOfThisUnit(arg1, isArg2NumberUnit ? undefined : arg2);
            if (converted !== undefined) {
                let out = isArg2NumberUnit ? arg2 : arg3;
                if (out) {
                    out.set(this.number - converted, this.unit);
                }
                else {
                    // eslint-disable-next-line @typescript-eslint/no-this-alias
                    out = this;
                    this._number -= converted;
                }
                return out;
            }
            throw new Error('Invalid unit conversion attempted.');
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        scale(factor, out) {
            if (out) {
                return out.set(this.number * factor, this.unit);
            }
            else {
                this._number *= factor;
                return this;
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        ratio(value, unit) {
            const converted = this.toNumberOfThisUnit(value, unit);
            if (converted) {
                return this.number / converted;
            }
            throw new Error('Invalid unit conversion attempted.');
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        abs(out) {
            if (out) {
                return out.set(Math.abs(this.number), this.unit);
            }
            else {
                this._number = Math.abs(this._number);
                return this;
            }
        }
        /**
         * Returns the numeric value of this NumberUnit after conversion to a specified unit.
         * @param unit The unit to which to convert.
         * @returns The converted numeric value.
         * @throws Error if this NumberUnit's unit type cannot be converted to the specified unit.
         */
        asUnit(unit) {
            return this.unit.convertTo(this.number, unit);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        compare(value, unit) {
            const converted = this.toNumberOfThisUnit(value, unit);
            if (converted === undefined) {
                throw new Error('Invalid unit conversion attempted.');
            }
            const diff = this.number - converted;
            if (Math.abs(diff) < 1e-14) {
                return 0;
            }
            return Math.sign(diff);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        equals(value, unit) {
            const converted = this.toNumberOfThisUnit(value, unit);
            if (converted === undefined) {
                return false;
            }
            if (isNaN(converted) && this.isNaN()) {
                return true;
            }
            const diff = this.number - converted;
            return !isNaN(diff) && Math.abs(diff) < 1e-14;
        }
        /**
         * Checks whether this NumberUnit has a numeric value of NaN.
         * @returns Whether this NumberUnit has a numeric value of NaN.
         */
        isNaN() {
            return isNaN(this.number);
        }
        /**
         * Copies this NumberUnit.
         * @returns A copy of this NumberUnit.
         */
        copy() {
            return new NumberUnit(this.number, this.unit);
        }
    }
    /**
     * A read-only interface for a WT_NumberUnit.
     */
    class NumberUnitReadOnly {
        /**
         * Constructor.
         * @param source - the source of the new read-only NumberUnit.
         */
        constructor(source) {
            this.source = source;
        }
        /**
         * Gets this NumberUnit's numeric value.
         * @returns This NumberUnit's numeric value.
         */
        get number() {
            return this.source.number;
        }
        /**
         * Gets this NumberUnit's unit type.
         * @returns This NumberUnit's unit type.
         */
        get unit() {
            return this.source.unit;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        add(arg1, arg2, arg3) {
            const isArg2NumberUnit = arg2 instanceof NumberUnit;
            const out = (isArg2NumberUnit ? arg2 : arg3);
            if (typeof arg1 === 'number') {
                return this.source.add(arg1, arg2, out);
            }
            else {
                return this.source.add(arg1, out);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        subtract(arg1, arg2, arg3) {
            const isArg2NumberUnit = arg2 instanceof NumberUnit;
            const out = (isArg2NumberUnit ? arg2 : arg3);
            if (typeof arg1 === 'number') {
                return this.source.subtract(arg1, arg2, out);
            }
            else {
                return this.source.subtract(arg1, out);
            }
        }
        /**
         * Scales this NumberUnit by a unit-less factor and returns the result.
         * @param factor The factor by which to scale.
         * @param out The NumberUnit to which to write the result.
         * @returns The scaled value.
         */
        scale(factor, out) {
            return this.source.scale(factor, out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        ratio(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.ratio(arg1, arg2);
            }
            else {
                return this.source.ratio(arg1);
            }
        }
        /**
         * Calculates the absolute value of this NumberUnit and returns the result.
         * @param out The NumberUnit to which to write the result.
         * @returns The absolute value.
         */
        abs(out) {
            return this.source.abs(out);
        }
        /**
         * Returns the numeric value of this NumberUnit after conversion to a specified unit.
         * @param unit The unit to which to convert.
         * @returns The converted numeric value.
         * @throws Error if this NumberUnit's unit type cannot be converted to the specified unit.
         */
        asUnit(unit) {
            return this.source.asUnit(unit);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        compare(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.compare(arg1, arg2);
            }
            else {
                return this.source.compare(arg1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        equals(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.equals(arg1, arg2);
            }
            else {
                return this.source.equals(arg1);
            }
        }
        /**
         * Checks whether this NumberUnit has a numeric value of NaN.
         * @returns Whether this NumberUnit has a numeric value of NaN.
         */
        isNaN() {
            return this.source.isNaN();
        }
        /**
         * Copies this NumberUnit.
         * @returns A copy of this NumberUnit.
         */
        copy() {
            return this.source.copy();
        }
    }
    /**
     * A unit of measurement.
     */
    class AbstractUnit {
        /**
         * Constructor.
         * @param name The name of this unit.
         */
        constructor(name) {
            this.name = name;
        }
        /** @inheritdoc */
        canConvert(otherUnit) {
            return this.family === otherUnit.family;
        }
        /** @inheritdoc */
        createNumber(value) {
            return new NumberUnit(value, this);
        }
        /** @inheritdoc */
        equals(other) {
            return this.family === other.family && this.name === other.name;
        }
    }
    /**
     * A unit that can be converted to another unit of the same type via a fixed linear transformation.
     */
    class SimpleUnit extends AbstractUnit {
        /**
         * Constructor.
         * @param family The family to which this unit belongs.
         * @param name The name of this unit.
         * @param scaleFactor The relative linear scale of the new unit compared to the standard unit of the same family.
         * @param zeroOffset The zero offset of the new unit compared to the standard unit of the same family.
         */
        constructor(family, name, scaleFactor, zeroOffset = 0) {
            super(name);
            this.family = family;
            this.scaleFactor = scaleFactor;
            this.zeroOffset = zeroOffset;
        }
        /** @inheritdoc */
        canConvert(otherUnit) {
            return otherUnit instanceof SimpleUnit && super.canConvert(otherUnit);
        }
        /** @inheritdoc */
        convertTo(value, toUnit) {
            if (!this.canConvert(toUnit)) {
                throw new Error(`Invalid conversion from ${this.name} to ${toUnit.name}.`);
            }
            return (value + this.zeroOffset) * (this.scaleFactor / toUnit.scaleFactor) - toUnit.zeroOffset;
        }
        /** @inheritdoc */
        convertFrom(value, fromUnit) {
            if (!this.canConvert(fromUnit)) {
                throw new Error(`Invalid conversion from ${fromUnit.name} to ${this.name}.`);
            }
            return (value + fromUnit.zeroOffset) * (fromUnit.scaleFactor / this.scaleFactor) - this.zeroOffset;
        }
    }
    /**
     * A unit of measure composed of the multiplicative combination of multiple elementary units.
     */
    class CompoundUnit extends AbstractUnit {
        /**
         * Constructor.
         * @param family The family to which this unit belongs.
         * @param numerator An array of CompoundableUnits containing all the units in the numerator of the compound unit.
         * @param denominator An array of CompoundableUnits containing all the units in the denominator of the compound unit.
         * @param name The name of this unit. If not defined, one will be automatically generated.
         */
        constructor(family, numerator, denominator, name) {
            // if not specified, build name from component units.
            if (name === undefined) {
                name = '';
                let i = 0;
                while (i < numerator.length - 1) {
                    name += `${numerator[i++].name}-`;
                }
                name += `${numerator[i].name}`;
                if (denominator.length > 0) {
                    name += ' per ';
                    i = 0;
                    while (i < denominator.length - 1) {
                        name += `${denominator[i++].name}-`;
                    }
                    name += `${denominator[i].name}`;
                }
            }
            super(name);
            this.family = family;
            this.numerator = Array.from(numerator);
            this.denominator = Array.from(denominator);
            this.numerator.sort((a, b) => a.family.localeCompare(b.family));
            this.denominator.sort((a, b) => a.family.localeCompare(b.family));
            this.scaleFactor = this.getScaleFactor();
        }
        /**
         * Gets the scale factor for this unit.
         * @returns the scale factor for this unit.
         */
        getScaleFactor() {
            let factor = 1;
            factor = this.numerator.reduce((prev, curr) => prev * curr.scaleFactor, factor);
            factor = this.denominator.reduce((prev, curr) => prev / curr.scaleFactor, factor);
            return factor;
        }
        /** @inheritdoc */
        canConvert(otherUnit) {
            return otherUnit instanceof CompoundUnit && super.canConvert(otherUnit);
        }
        /** @inheritdoc */
        convertTo(value, toUnit) {
            if (!this.canConvert(toUnit)) {
                throw new Error(`Invalid conversion from ${this.name} to ${toUnit.name}.`);
            }
            return value * (this.scaleFactor / toUnit.scaleFactor);
        }
        /** @inheritdoc */
        convertFrom(value, fromUnit) {
            if (!this.canConvert(fromUnit)) {
                throw new Error(`Invalid conversion from ${fromUnit.name} to ${this.name}.`);
            }
            return value * (fromUnit.scaleFactor / this.scaleFactor);
        }
    }
    /**
     * Predefined unit families.
     */
    exports.UnitFamily = void 0;
    (function (UnitFamily) {
        UnitFamily["Distance"] = "distance";
        UnitFamily["Angle"] = "angle";
        UnitFamily["Duration"] = "duration";
        UnitFamily["Weight"] = "weight";
        UnitFamily["Mass"] = "weight";
        UnitFamily["Volume"] = "volume";
        UnitFamily["Pressure"] = "pressure";
        UnitFamily["Temperature"] = "temperature";
        UnitFamily["TemperatureDelta"] = "temperature_delta";
        UnitFamily["Speed"] = "speed";
        UnitFamily["Acceleration"] = "acceleration";
        UnitFamily["WeightFlux"] = "weight_flux";
        UnitFamily["MassFlux"] = "weight_flux";
        UnitFamily["VolumeFlux"] = "volume_flux";
        UnitFamily["Density"] = "density";
        UnitFamily["Force"] = "force";
        UnitFamily["DistancePerWeight"] = "distance_per_weight";
        UnitFamily["DistanceRatio"] = "distance_ratio";
        UnitFamily["WeightPerDistance"] = "weight_per_distance";
    })(exports.UnitFamily || (exports.UnitFamily = {}));
    /**
     * Predefined unit types.
     */
    class UnitType {
    }
    UnitType.METER = new SimpleUnit(exports.UnitFamily.Distance, 'meter', 1);
    UnitType.CENTIMETER = new SimpleUnit(exports.UnitFamily.Distance, 'centimeter', 0.01);
    UnitType.KILOMETER = new SimpleUnit(exports.UnitFamily.Distance, 'kilometer', 1000);
    UnitType.INCH = new SimpleUnit(exports.UnitFamily.Distance, 'inch', 0.0254);
    UnitType.FOOT = new SimpleUnit(exports.UnitFamily.Distance, 'foot', 0.3048);
    /** Statute mile. */
    UnitType.MILE = new SimpleUnit(exports.UnitFamily.Distance, 'mile', 1609.34);
    /** Nautical mile. */
    UnitType.NMILE = new SimpleUnit(exports.UnitFamily.Distance, 'nautical mile', 1852);
    /** Great-arc radian. The average radius of Earth. */
    UnitType.GA_RADIAN = new SimpleUnit(exports.UnitFamily.Distance, 'great arc radian', 6378100);
    /** 9.80665 meters, for internal use. */
    UnitType.G_METER = new SimpleUnit(exports.UnitFamily.Distance, '9.80665 meter', 9.80665);
    UnitType.RADIAN = new SimpleUnit(exports.UnitFamily.Angle, 'radian', 1);
    UnitType.DEGREE = new SimpleUnit(exports.UnitFamily.Angle, 'degree', Math.PI / 180);
    UnitType.ARC_MIN = new SimpleUnit(exports.UnitFamily.Angle, 'minute', Math.PI / 180 / 60);
    UnitType.ARC_SEC = new SimpleUnit(exports.UnitFamily.Angle, 'second', Math.PI / 180 / 3600);
    UnitType.MILLISECOND = new SimpleUnit(exports.UnitFamily.Duration, 'millisecond', 0.001);
    UnitType.SECOND = new SimpleUnit(exports.UnitFamily.Duration, 'second', 1);
    UnitType.MINUTE = new SimpleUnit(exports.UnitFamily.Duration, 'minute', 60);
    UnitType.HOUR = new SimpleUnit(exports.UnitFamily.Duration, 'hour', 3600);
    UnitType.KILOGRAM = new SimpleUnit(exports.UnitFamily.Weight, 'kilogram', 1);
    UnitType.POUND = new SimpleUnit(exports.UnitFamily.Weight, 'pound', 0.453592);
    UnitType.SLUG = new SimpleUnit(exports.UnitFamily.Weight, 'slug', 14.59390);
    UnitType.TON = new SimpleUnit(exports.UnitFamily.Weight, 'ton', 907.185);
    UnitType.TONNE = new SimpleUnit(exports.UnitFamily.Weight, 'tonne', 1000);
    /** Weight equivalent of one liter of fuel, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.LITER_FUEL = new SimpleUnit(exports.UnitFamily.Weight, 'liter', 0.80283679);
    /** Weight equivalent of one gallon of fuel, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.GALLON_FUEL = new SimpleUnit(exports.UnitFamily.Weight, 'gallon', 3.0390664);
    /** Weight equivalent of one liter of fuel, using the generic conversion 1 gallon = 6.0 pounds. */
    UnitType.LITER_AUTOGAS_FUEL = new SimpleUnit(exports.UnitFamily.Weight, 'liter', 0.71895832);
    /** Weight equivalent of one gallon of fuel, using the generic conversion 1 gallon = 6.0 pounds. */
    UnitType.GALLON_AUTOGAS_FUEL = new SimpleUnit(exports.UnitFamily.Weight, 'gallon', 2.721552);
    /** Weight equivalent of one imperial gallon of fuel, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.IMP_GALLON_FUEL = new SimpleUnit(exports.UnitFamily.Weight, 'imperial gallon', 3.6497683);
    UnitType.LITER = new SimpleUnit(exports.UnitFamily.Volume, 'liter', 1);
    UnitType.GALLON = new SimpleUnit(exports.UnitFamily.Volume, 'gallon', 3.78541);
    /** Hectopascal. */
    UnitType.HPA = new SimpleUnit(exports.UnitFamily.Pressure, 'hectopascal', 1);
    /** Millibar. */
    UnitType.MB = new SimpleUnit(exports.UnitFamily.Pressure, 'millibar', 1);
    /** Atmosphere. */
    UnitType.ATM = new SimpleUnit(exports.UnitFamily.Pressure, 'atmosphere', 1013.25);
    /** Inch of mercury. */
    UnitType.IN_HG = new SimpleUnit(exports.UnitFamily.Pressure, 'inch of mercury', 33.8639);
    /** Millimeter of mercury. */
    UnitType.MM_HG = new SimpleUnit(exports.UnitFamily.Pressure, 'millimeter of mercury', 1.33322);
    /** Pound per square inch. */
    UnitType.PSI = new SimpleUnit(exports.UnitFamily.Pressure, 'pound per square inch', 68.9476);
    UnitType.KELVIN = new SimpleUnit(exports.UnitFamily.Temperature, 'kelvin', 1, 0);
    UnitType.CELSIUS = new SimpleUnit(exports.UnitFamily.Temperature, '° Celsius', 1, 273.15);
    UnitType.FAHRENHEIT = new SimpleUnit(exports.UnitFamily.Temperature, '° Fahrenheit', 5 / 9, 459.67);
    UnitType.RANKINE = new SimpleUnit(exports.UnitFamily.Temperature, '° Rankine', 5 / 9, 0);
    /** Change in degrees Celsius. */
    UnitType.DELTA_CELSIUS = new SimpleUnit(exports.UnitFamily.TemperatureDelta, 'Δ° Celsius', 1);
    /** Change in degrees Fahrenheit. */
    UnitType.DELTA_FAHRENHEIT = new SimpleUnit(exports.UnitFamily.TemperatureDelta, 'Δ° Fahrenheit', 5 / 9);
    UnitType.KNOT = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.NMILE], [UnitType.HOUR], 'knot');
    /** Kilometer per hour. */
    UnitType.KPH = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.KILOMETER], [UnitType.HOUR]);
    /** Miles per hour. */
    UnitType.MPH = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.MILE], [UnitType.HOUR]);
    /** Meter per minute. */
    UnitType.MPM = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.METER], [UnitType.MINUTE]);
    /** Meter per second. */
    UnitType.MPS = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.METER], [UnitType.SECOND]);
    /** Foot per minute. */
    UnitType.FPM = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.FOOT], [UnitType.MINUTE]);
    /** Foot per second. */
    UnitType.FPS = new CompoundUnit(exports.UnitFamily.Speed, [UnitType.FOOT], [UnitType.SECOND]);
    /** Meter per minute per second. */
    UnitType.MPM_PER_SEC = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.METER], [UnitType.MINUTE, UnitType.SECOND]);
    /** Meter per second per second. */
    UnitType.MPS_PER_SEC = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.METER], [UnitType.SECOND, UnitType.SECOND]);
    /** Foot per minute per second. */
    UnitType.FPM_PER_SEC = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.FOOT], [UnitType.MINUTE, UnitType.SECOND]);
    /** Foot per second per second. */
    UnitType.FPS_PER_SEC = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.FOOT], [UnitType.SECOND, UnitType.SECOND]);
    /** Knot per second. */
    UnitType.KNOT_PER_SEC = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.NMILE], [UnitType.HOUR, UnitType.SECOND]);
    /** Average gravitational acceleration on Earth at sea level. */
    UnitType.G_ACCEL = new CompoundUnit(exports.UnitFamily.Acceleration, [UnitType.G_METER], [UnitType.SECOND, UnitType.SECOND]);
    /** Kilogram per hour. */
    UnitType.KGH = new CompoundUnit(exports.UnitFamily.WeightFlux, [UnitType.KILOGRAM], [UnitType.HOUR]);
    /** Pound per hour. */
    UnitType.PPH = new CompoundUnit(exports.UnitFamily.WeightFlux, [UnitType.POUND], [UnitType.HOUR]);
    /** Weight equivalent of one liter of fuel per hour, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.LPH_FUEL = new CompoundUnit(exports.UnitFamily.WeightFlux, [UnitType.LITER_FUEL], [UnitType.HOUR]);
    /** Weight equivalent of one gallon of fuel per hour, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.GPH_FUEL = new CompoundUnit(exports.UnitFamily.WeightFlux, [UnitType.GALLON_FUEL], [UnitType.HOUR]);
    /** Weight equivalent of one imperial gallon of fuel per hour, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.IGPH_FUEL = new CompoundUnit(exports.UnitFamily.WeightFlux, [UnitType.IMP_GALLON_FUEL], [UnitType.HOUR]);
    /** Density in slugs per cubic foot */
    UnitType.SLUG_PER_FT3 = new CompoundUnit(exports.UnitFamily.Density, [UnitType.SLUG], [UnitType.FOOT, UnitType.FOOT, UnitType.FOOT]);
    /** Density in kilograms per cubic meter */
    UnitType.KG_PER_M3 = new CompoundUnit(exports.UnitFamily.Density, [UnitType.KILOGRAM], [UnitType.METER, UnitType.METER, UnitType.METER]);
    /** Newton. */
    UnitType.NEWTON = new CompoundUnit(exports.UnitFamily.Force, [UnitType.KILOGRAM, UnitType.METER], [UnitType.SECOND, UnitType.SECOND]);
    /** Pound (force). */
    UnitType.POUND_FORCE = new CompoundUnit(exports.UnitFamily.Force, [UnitType.POUND, UnitType.G_METER], [UnitType.SECOND, UnitType.SECOND]);
    /** One statute mile per weight equivalent of one gallon of fuel, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.MILE_PER_GALLON_FUEL = new CompoundUnit(exports.UnitFamily.DistancePerWeight, [UnitType.MILE], [UnitType.GALLON_FUEL]);
    /** One nautical mile per weight equivalent of one gallon of fuel, using the generic conversion 1 gallon = 6.7 pounds. */
    UnitType.NMILE_PER_GALLON_FUEL = new CompoundUnit(exports.UnitFamily.DistancePerWeight, [UnitType.NMILE], [UnitType.GALLON_FUEL]);
    /** One foot per nautical mile. */
    UnitType.FOOT_PER_NMILE = new CompoundUnit(exports.UnitFamily.DistanceRatio, [UnitType.FOOT], [UnitType.NMILE]);

    /**
     * A publisher for air data computer information.
     */
    class AdcPublisher extends SimVarPublisher {
        /**
         * Creates an AdcPublisher.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            const simvars = new Map([
                ['indicated_alt', { name: 'INDICATED ALTITUDE:#index#', type: exports.SimVarValueType.Feet, indexed: true }],
                ['altimeter_baro_setting_inhg', { name: 'KOHLSMAN SETTING HG:#index#', type: exports.SimVarValueType.InHG, indexed: true }],
                ['altimeter_baro_setting_mb', { name: 'KOHLSMAN SETTING MB:#index#', type: exports.SimVarValueType.MB, indexed: true }],
                ['altimeter_baro_preselect_raw', { name: 'L:XMLVAR_Baro#index#_SavedPressure', type: exports.SimVarValueType.Number, indexed: true }],
                ['altimeter_baro_preselect_inhg', { name: 'L:XMLVAR_Baro#index#_SavedPressure', type: exports.SimVarValueType.Number, map: raw => UnitType.HPA.convertTo(raw / 16, UnitType.IN_HG), indexed: true }],
                ['altimeter_baro_preselect_mb', { name: 'L:XMLVAR_Baro#index#_SavedPressure', type: exports.SimVarValueType.Number, map: raw => raw / 16, indexed: true }],
                ['altimeter_baro_is_std', { name: 'L:XMLVAR_Baro#index#_ForcedToSTD', type: exports.SimVarValueType.Bool, indexed: true }],
                ['radio_alt', { name: 'RADIO HEIGHT', type: exports.SimVarValueType.Feet }],
                ['pressure_alt', { name: 'PRESSURE ALTITUDE', type: exports.SimVarValueType.Feet }],
                ['vertical_speed', { name: 'VERTICAL SPEED', type: exports.SimVarValueType.FPM }],
                ['ambient_density', { name: 'AMBIENT DENSITY', type: exports.SimVarValueType.SlugsPerCubicFoot }],
                ['isa_temp_c', { name: 'STANDARD ATM TEMPERATURE', type: exports.SimVarValueType.Celsius }],
                ['ram_air_temp_c', { name: 'TOTAL AIR TEMPERATURE', type: exports.SimVarValueType.Celsius }],
                ['ambient_wind_velocity', { name: 'AMBIENT WIND VELOCITY', type: exports.SimVarValueType.Knots }],
                ['ambient_wind_direction', { name: 'AMBIENT WIND DIRECTION', type: exports.SimVarValueType.Degree }],
                ['on_ground', { name: 'SIM ON GROUND', type: exports.SimVarValueType.Bool }],
                ['aoa', { name: 'INCIDENCE ALPHA', type: exports.SimVarValueType.Degree }],
                ['stall_aoa', { name: 'STALL ALPHA', type: exports.SimVarValueType.Degree }],
                ['zero_lift_aoa', { name: 'ZERO LIFT ALPHA', type: exports.SimVarValueType.Degree }],
                ['density_alt', { name: 'DENSITY ALTITUDE', type: exports.SimVarValueType.Feet }],
                ['dynamic_pressure_inhg', { name: 'DYNAMIC PRESSURE', type: exports.SimVarValueType.InHG }],
            ]);
            super(simvars, bus, pacer);
            this.registeredSimVarIds = {
                mach: SimVar.GetRegisteredId('AIRSPEED MACH', exports.SimVarValueType.Mach, ''),
                ambientPressure: SimVar.GetRegisteredId('AMBIENT PRESSURE', exports.SimVarValueType.InHG, ''),
                ambientTemperature: SimVar.GetRegisteredId('AMBIENT TEMPERATURE', exports.SimVarValueType.Celsius, ''),
            };
            (_a = this.needPublish) !== null && _a !== void 0 ? _a : (this.needPublish = {
                'mach_number': false,
                'ambient_pressure_inhg': false,
                'ambient_temp_c': false
            });
            (_b = this.needPublishIasTopics) !== null && _b !== void 0 ? _b : (this.needPublishIasTopics = new Map());
            (_c = this.needRetrievePressure) !== null && _c !== void 0 ? _c : (this.needRetrievePressure = false);
            (_d = this.needRetrieveTemperature) !== null && _d !== void 0 ? _d : (this.needRetrieveTemperature = false);
            (_e = this.needRetrieveMach) !== null && _e !== void 0 ? _e : (this.needRetrieveMach = false);
            (_f = this.pressure) !== null && _f !== void 0 ? _f : (this.pressure = 1013.25);
            (_g = this.temperature) !== null && _g !== void 0 ? _g : (this.temperature = 0);
            (_h = this.mach) !== null && _h !== void 0 ? _h : (this.mach = 0);
        }
        /** @inheritDoc */
        handleSubscribedTopic(topic) {
            var _a, _b;
            (_a = this.needPublish) !== null && _a !== void 0 ? _a : (this.needPublish = {
                'mach_number': false,
                'ambient_pressure_inhg': false,
                'ambient_temp_c': false
            });
            (_b = this.needPublishIasTopics) !== null && _b !== void 0 ? _b : (this.needPublishIasTopics = new Map());
            if (this.resolvedSimVars.has(topic)
                || topic in this.needPublish
                || AdcPublisher.TOPIC_REGEXES['ias'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['tas'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['indicated_mach_number'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['indicated_tas'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['mach_to_kias_factor'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['tas_to_ias_factor'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['indicated_mach_to_kias_factor'].test(topic)
                || AdcPublisher.TOPIC_REGEXES['indicated_tas_to_ias_factor'].test(topic)) {
                // If topic matches an already resolved topic -> start publishing.
                this.onTopicSubscribed(topic);
            }
            else {
                // Check if topic matches indexed topic.
                this.tryMatchIndexedSubscribedTopic(topic);
            }
        }
        /** @inheritDoc */
        onTopicSubscribed(topic) {
            if (topic in this.needPublish) {
                this.needPublish[topic] = true;
                switch (topic) {
                    case 'ambient_pressure_inhg':
                        this.needRetrievePressure = true;
                        if (this.publishActive) {
                            this.retrieveAmbientPressure(true);
                        }
                        break;
                    case 'ambient_temp_c':
                        this.needRetrieveTemperature = true;
                        if (this.publishActive) {
                            this.retrieveAmbientTemperature(true);
                        }
                        break;
                    case 'mach_number':
                        this.needRetrieveMach = true;
                        if (this.publishActive) {
                            this.retrieveMach(true);
                        }
                        break;
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['ias'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['ias'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.iasTopic = index < 0 ? 'ias' : `ias_${index}`;
                if (this.publishActive) {
                    this.retrieveIas(entry, true);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['tas'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['tas'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.tasTopic = index < 0 ? 'tas' : `tas_${index}`;
                if (this.publishActive) {
                    this.retrieveTas(entry, true);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['indicated_mach_number'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['indicated_mach_number'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.indicatedMachTopic = index < 0 ? 'indicated_mach_number' : `indicated_mach_number_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveIas(entry, false);
                    this.retrieveIndicatedMach(entry, true);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['indicated_tas'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['indicated_tas'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                this.needRetrieveTemperature = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.indicatedTasTopic = index < 0 ? 'indicated_tas' : `indicated_tas_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveAmbientTemperature(false);
                    this.retrieveIas(entry, false);
                    this.retrieveIndicatedTas(entry, true);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['mach_to_kias_factor'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['mach_to_kias_factor'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                this.needRetrieveMach = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.machToKiasTopic = index < 0 ? 'mach_to_kias_factor' : `mach_to_kias_factor_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveMach(false);
                    this.retrieveIas(entry, false);
                    this.publishMachToKias(entry);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['tas_to_ias_factor'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['tas_to_ias_factor'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                this.needRetrieveTemperature = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.tasToIasTopic = index < 0 ? 'tas_to_ias_factor' : `tas_to_ias_factor_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveAmbientTemperature(false);
                    this.retrieveIas(entry, false);
                    this.retrieveTas(entry, false);
                    this.publishTasToIas(entry);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['indicated_mach_to_kias_factor'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['indicated_mach_to_kias_factor'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.indicatedMachToKiasTopic = index < 0 ? 'indicated_mach_to_kias_factor' : `indicated_mach_to_kias_factor_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveIas(entry, false);
                    this.retrieveIndicatedMach(entry, false);
                    this.publishIndicatedMachToKias(entry);
                }
            }
            else if (AdcPublisher.TOPIC_REGEXES['indicated_tas_to_ias_factor'].test(topic)) {
                const indexMatch = topic.match(AdcPublisher.TOPIC_REGEXES['indicated_tas_to_ias_factor'])[1];
                const index = indexMatch ? parseInt(indexMatch) : -1;
                this.needRetrievePressure = true;
                this.needRetrieveTemperature = true;
                const entry = this.getOrCreateIasTopicEntry(index);
                entry.indicatedTasToIasTopic = index < 0 ? 'indicated_tas_to_ias_factor' : `indicated_tas_to_ias_factor_${index}`;
                if (this.publishActive) {
                    this.retrieveAmbientPressure(false);
                    this.retrieveAmbientTemperature(false);
                    this.retrieveIas(entry, false);
                    this.retrieveIndicatedTas(entry, false);
                    this.publishIndicatedTasToIas(entry);
                }
            }
            else {
                super.onTopicSubscribed(topic);
            }
        }
        /**
         * Gets the entry describing indicated airspeed-related topics to publish for a given airspeed indicator index, or
         * creates a new one if it does not exist.
         * @param index The airspeed indicator index for which to get an entry, or `-1` for the un-indexed airspeed
         * indicator.
         * @returns An entry describing indicated airspeed-related topics to publish for the specified airspeed indicator
         * index.
         */
        getOrCreateIasTopicEntry(index) {
            let entry = this.needPublishIasTopics.get(index);
            if (!entry) {
                entry = {
                    iasSimVarId: SimVar.GetRegisteredId(index < 0 ? 'AIRSPEED INDICATED:1' : `AIRSPEED INDICATED:${index}`, exports.SimVarValueType.Knots, ''),
                    tasSimVarId: SimVar.GetRegisteredId(index < 0 ? 'AIRSPEED TRUE:1' : `AIRSPEED TRUE:${index}`, exports.SimVarValueType.Knots, ''),
                    kias: 0,
                    iasMps: 0,
                    ktas: 0,
                    indicatedMach: 0,
                    indicatedTas: 0
                };
                this.needPublishIasTopics.set(index, entry);
            }
            return entry;
        }
        /** @inheritDoc */
        onUpdate() {
            const isSlewing = SimVar.GetSimVarValue('IS SLEW ACTIVE', 'bool');
            if (!isSlewing) {
                if (this.needRetrievePressure) {
                    this.retrieveAmbientPressure(this.needPublish['ambient_pressure_inhg']);
                }
                if (this.needRetrieveTemperature) {
                    this.retrieveAmbientTemperature(this.needPublish['ambient_temp_c']);
                }
                if (this.needRetrieveMach) {
                    this.retrieveMach(this.needPublish['mach_number']);
                }
                for (const entry of this.needPublishIasTopics.values()) {
                    this.retrieveIas(entry, true);
                    if (entry.tasTopic || entry.tasToIasTopic) {
                        this.retrieveTas(entry, true);
                    }
                    if (entry.indicatedMachTopic || entry.indicatedMachToKiasTopic) {
                        this.retrieveIndicatedMach(entry, true);
                    }
                    if (entry.indicatedTasTopic || entry.indicatedTasToIasTopic) {
                        this.retrieveIndicatedTas(entry, true);
                    }
                    this.publishMachToKias(entry);
                    this.publishTasToIas(entry);
                    this.publishIndicatedMachToKias(entry);
                    this.publishIndicatedTasToIas(entry);
                }
                super.onUpdate();
            }
        }
        /**
         * Retrieves and optionally publishes the current ambient pressure.
         * @param publish Whether to publish the value.
         */
        retrieveAmbientPressure(publish) {
            const pressureInHg = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.ambientPressure);
            this.pressure = UnitType.IN_HG.convertTo(pressureInHg, UnitType.HPA);
            publish && this.publish('ambient_pressure_inhg', pressureInHg);
        }
        /**
         * Retrieves and optionally publishes the current ambient temperature.
         * @param publish Whether to publish the value.
         */
        retrieveAmbientTemperature(publish) {
            this.temperature = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.ambientTemperature);
            publish && this.publish('ambient_temp_c', this.temperature);
        }
        /**
         * Retrieves and optionally publishes the airplane's current mach number.
         * @param publish Whether to publish the value.
         */
        retrieveMach(publish) {
            this.mach = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.mach);
            publish && this.publish('mach_number', this.mach);
        }
        /**
         * Retrieves and optionally publishes the current indicated airspeed for an airspeed indicator index.
         * @param entry The entry for the airspeed indicator index for which to retrieve the value.
         * @param publish Whether to publish the value.
         */
        retrieveIas(entry, publish) {
            entry.kias = SimVar.GetSimVarValueFastReg(entry.iasSimVarId);
            entry.iasMps = UnitType.KNOT.convertTo(entry.kias, UnitType.MPS);
            publish && entry.iasTopic && this.publish(entry.iasTopic, entry.kias);
        }
        /**
         * Retrieves and optionally publishes the current true airspeed for an airspeed indicator index.
         * @param entry The entry for the airspeed indicator index for which to retrieve the value.
         * @param publish Whether to publish the value.
         */
        retrieveTas(entry, publish) {
            entry.ktas = SimVar.GetSimVarValueFastReg(entry.tasSimVarId);
            publish && entry.tasTopic && this.publish(entry.tasTopic, entry.ktas);
        }
        /**
         * Retrieves and optionally publishes the current indicated mach number for an airspeed indicator index.
         * @param entry The entry for the airspeed indicator index for which to retrieve the value.
         * @param publish Whether to publish the value.
         */
        retrieveIndicatedMach(entry, publish) {
            entry.indicatedMach = AeroMath.casToMach(entry.iasMps, this.pressure);
            publish && entry.indicatedMachTopic && this.publish(entry.indicatedMachTopic, entry.indicatedMach);
        }
        /**
         * Retrieves and optionally publishes the current indicated true airspeed for an airspeed indicator index.
         * @param entry The entry for the airspeed indicator index for which to retrieve the value.
         * @param publish Whether to publish the value.
         */
        retrieveIndicatedTas(entry, publish) {
            entry.indicatedTas = UnitType.MPS.convertTo(AeroMath.casToTas(entry.iasMps, this.pressure, this.temperature), UnitType.KNOT);
            publish && entry.indicatedTasTopic && this.publish(entry.indicatedTasTopic, entry.indicatedTas);
        }
        /**
         * Publishes the current conversion factor from mach number to knots indicated airspeed for an airspeed indicator
         * index.
         * @param entry The entry for the airspeed indicator index for which to publish the value.
         */
        publishMachToKias(entry) {
            if (!entry.machToKiasTopic) {
                return;
            }
            const factor = entry.kias < 1 || this.mach === 0
                // 1 m/s = 1.943844492440605 knots
                ? AeroMath.machToCas(1, this.pressure) * 1.943844492440605
                : entry.kias / this.mach;
            this.publish(entry.machToKiasTopic, isFinite(factor) ? factor : 1);
        }
        /**
         * Publishes the current conversion factor from true airspeed to indicated airspeed for an airspeed indicator index.
         * @param entry The entry for the airspeed indicator index for which to publish the value.
         */
        publishTasToIas(entry) {
            if (!entry.tasToIasTopic) {
                return;
            }
            const factor = entry.kias < 1 || entry.ktas === 0
                ? 1 / AeroMath.casToTas(1, this.pressure, this.temperature)
                : entry.kias / entry.ktas;
            this.publish(entry.tasToIasTopic, isFinite(factor) ? factor : 1);
        }
        /**
         * Publishes the current conversion factor from indicated mach number to knots indicated airspeed for an airspeed
         * indicator index.
         * @param entry The entry for the airspeed indicator index for which to publish the value.
         */
        publishIndicatedMachToKias(entry) {
            if (!entry.indicatedMachToKiasTopic) {
                return;
            }
            const factor = entry.kias < 1 || entry.indicatedMach === 0
                // 1 m/s = 1.943844492440605 knots
                ? AeroMath.machToCas(1, this.pressure) * 1.943844492440605
                : entry.kias / entry.indicatedMach;
            this.publish(entry.indicatedMachToKiasTopic, isFinite(factor) ? factor : 1);
        }
        /**
         * Publishes the current conversion factor from indicated true airspeed to indicated airspeed for an airspeed
         * indicator index.
         * @param entry The entry for the airspeed indicator index for which to publish the value.
         */
        publishIndicatedTasToIas(entry) {
            if (!entry.indicatedTasToIasTopic) {
                return;
            }
            const factor = entry.kias < 1 || entry.indicatedTas === 0
                ? 1 / AeroMath.casToTas(1, this.pressure, this.temperature)
                : entry.kias / entry.indicatedTas;
            this.publish(entry.indicatedTasToIasTopic, isFinite(factor) ? factor : 1);
        }
    }
    AdcPublisher.TOPIC_REGEXES = {
        'ias': /^ias(?:_(0|(?:[1-9])\d*))?$/,
        'tas': /^tas(?:_(0|(?:[1-9])\d*))?$/,
        'indicated_mach_number': /^indicated_mach_number(?:_(0|(?:[1-9])\d*))?$/,
        'indicated_tas': /^indicated_tas(?:_(0|(?:[1-9])\d*))?$/,
        'mach_to_kias_factor': /^mach_to_kias_factor(?:_(0|(?:[1-9])\d*))?$/,
        'tas_to_ias_factor': /^tas_to_ias_factor(?:_(0|(?:[1-9])\d*))?$/,
        'indicated_mach_to_kias_factor': /^indicated_mach_to_kias_factor(?:_(0|(?:[1-9])\d*))?$/,
        'indicated_tas_to_ias_factor': /^indicated_tas_to_ias_factor(?:_(0|(?:[1-9])\d*))?$/
    };

    /**
     * Rounding behaviors.
     */
    exports.Rounding = void 0;
    (function (Rounding) {
        Rounding[Rounding["Down"] = -1] = "Down";
        Rounding[Rounding["Nearest"] = 0] = "Nearest";
        Rounding[Rounding["Up"] = 1] = "Up";
    })(exports.Rounding || (exports.Rounding = {}));
    /**
     * A utitlity class for basic math.
     */
    class MathUtils {
        /**
         * Clamps a numerical value to the min/max range.
         * @param value The value to be clamped.
         * @param min The minimum.
         * @param max The maximum.
         *
         * @returns The clamped numerical value..
         */
        static clamp(value, min, max) {
            return Math.max(min, Math.min(max, value));
        }
        /**
         * Rounds a number.
         * @param value The number to round.
         * @param precision The precision with which to round. Defaults to `1`.
         * @returns The rounded number.
         */
        static round(value, precision = 1) {
            return Math.round(value / precision) * precision;
        }
        /**
         * Ceils a number.
         * @param value The number to ceil.
         * @param precision The precision with which to ceil. Defaults to `1`.
         * @returns The ceiled number.
         */
        static ceil(value, precision = 1) {
            return Math.ceil(value / precision) * precision;
        }
        /**
         * Floors a number.
         * @param value The number to floor.
         * @param precision The precision with which to floor. Defaults to `1`.
         * @returns The floored number.
         */
        static floor(value, precision = 1) {
            return Math.floor(value / precision) * precision;
        }
        /**
         * Normalizes an angle in radians.
         * @param radians The angle to normalize, in radians.
         * @param lowerBound The lower bound of the normalized angle, in radians. The normalized angle will fall in the
         * range `[lowerBound, lowerBound + 2 * pi)`. Defaults to `0`.
         * @returns The specified angle after normalization, in radians.
         */
        static normalizeAngle(radians, lowerBound = 0) {
            return (((radians - lowerBound) % MathUtils.TWO_PI) + MathUtils.TWO_PI) % MathUtils.TWO_PI + lowerBound;
        }
        /**
         * Normalizes an angle in degrees.
         * @param degrees The angle to normalize, in degrees.
         * @param lowerBound The lower bound of the normalized angle, in degrees. The normalized angle will fall in the
         * range `[lowerBound, lowerBound + 360)`. Defaults to `0`.
         * @returns The specified angle after normalization, in degrees.
         */
        static normalizeAngleDeg(degrees, lowerBound = 0) {
            return (((degrees - lowerBound) % 360) + 360) % 360 + lowerBound;
        }
        /**
         * Gets the angular distance, in radians, swept from an starting angle to an ending angle.
         * @param startRadians The starting angle, in radians.
         * @param endRadians The ending angle, in radians.
         * @param direction The direction to sweep from start to end. A positive value indicates sweeping in the direction
         * of increasing angle. A negative value indicates sweeping in the direction of decreasing angle. A value of zero
         * indicates that the direction should be chosen to produce the smallest angular distance.
         * @returns The angular distance, in radians, swept from the specified starting angle to the ending angle in the
         * specified direction.
         */
        static angularDistance(startRadians, endRadians, direction) {
            const sign = direction < 0 ? -1 : 1;
            const diff = MathUtils.normalizeAngle((endRadians - startRadians) * sign);
            return direction === 0 ? Math.min(diff, MathUtils.TWO_PI - diff) : diff;
        }
        /**
         * Gets the angular distance, in degrees, swept from an starting angle to an ending angle.
         * @param startDegrees The starting angle, in degrees.
         * @param endDegrees The ending angle, in degrees.
         * @param direction The direction to sweep from start to end. A positive value indicates sweeping in the direction
         * of increasing angle. A negative value indicates sweeping in the direction of decreasing angle. A value of zero
         * indicates that the direction should be chosen to produce the smallest angular distance.
         * @returns The angular distance, in degrees, swept from the specified starting angle to the ending angle in the
         * specified direction.
         */
        static angularDistanceDeg(startDegrees, endDegrees, direction) {
            const sign = direction < 0 ? -1 : 1;
            const diff = MathUtils.normalizeAngleDeg((endDegrees - startDegrees) * sign);
            return direction === 0 ? Math.min(diff, 360 - diff) : diff;
        }
        /**
         * Calculates the angular difference between two angles in the range `[0, 2 * pi)`. The calculation supports both
         * directional and non-directional differences. The directional difference is the angle swept from the start angle
         * to the end angle proceeding in the direction of increasing angle. The non-directional difference is the smaller
         * of the two angles swept from the start angle to the end angle proceeding in either direction.
         * @param start The starting angle, in radians.
         * @param end The ending angle, in radians.
         * @param directional Whether to calculate the directional difference. Defaults to `true`.
         * @returns The angular difference between the two angles, in radians, in the range `[0, 2 * pi)`.
         */
        static diffAngle(start, end, directional = true) {
            return MathUtils.angularDistance(start, end, directional ? 1 : 0);
        }
        /**
         * Calculates the angular difference between two angles in the range `[0, 360)`. The calculation supports both
         * directional and non-directional differences. The directional difference is the angle swept from the start angle
         * to the end angle proceeding in the direction of increasing angle. The non-directional difference is the smaller
         * of the two angles swept from the start angle to the end angle proceeding in either direction.
         * @param start The starting angle, in degrees.
         * @param end The ending angle, in degrees.
         * @param directional Whether to calculate the directional difference. Defaults to `true`.
         * @returns The angular difference between the two angles, in degrees, in the range `[0, 360)`.
         */
        static diffAngleDeg(start, end, directional = true) {
            return MathUtils.angularDistanceDeg(start, end, directional ? 1 : 0);
        }
        /**
         * Linearly interpolates a keyed value along one dimension.
         * @param x The key of the value to interpolate.
         * @param x0 The key of the first known value.
         * @param x1 The key of the second known value.
         * @param y0 The first known value.
         * @param y1 The second known value.
         * @param clampStart Whether to clamp the interpolated value to the first known value. Defaults to false.
         * @param clampEnd Whether to clamp the interpolated value to the second known value. Defaults to false.
         * @returns The interpolated value corresponding to the specified key.
         */
        static lerp(x, x0, x1, y0, y1, clampStart = false, clampEnd = false) {
            if (x0 !== x1 && y0 !== y1) {
                const fraction = MathUtils.clamp((x - x0) / (x1 - x0), clampStart ? 0 : -Infinity, clampEnd ? 1 : Infinity);
                return fraction * (y1 - y0) + y0;
            }
            else {
                return y0;
            }
        }
        /**
         * Linearly interpolates a keyed vector along one dimension. If the known vectors and the result vector have unequal
         * lengths, then only the components shared by all vectors are interpolated in the result.
         * @param out The object to which to write the result.
         * @param x The key of the vector to interpolate.
         * @param x0 The key of the first known vector.
         * @param x1 The key of the second known vector.
         * @param y0 The first known vector.
         * @param y1 The second known vector.
         * @param clampStart Whether to clamp the components of the interpolated vector to those of the first known vector.
         * Defaults to false.
         * @param clampEnd Whether to clamp the components of the interpolated vector to those of the second known vector.
         * Defaults to false.
         * @returns The interpolated vector corresponding to the specified key.
         */
        static lerpVector(out, x, x0, x1, y0, y1, clampStart = false, clampEnd = false) {
            const length = Math.min(y0.length, y1.length, out.length);
            for (let i = 0; i < length; i++) {
                out[i] = MathUtils.lerp(x, x0, x1, y0[i], y1[i], clampStart, clampEnd);
            }
            return out;
        }
        /**
         * Gets the sign of a number, including 0.
         * @param n The number to get the sign of.
         * @returns 1.0 if the number is positive, +0 or Infinity;
          -1.0 if the number is negative, -0 or -Infinity;
          NaN if the number is NaN
         */
        static hardSign(n) {
            return isNaN(n) ? NaN : (n < 0 || Object.is(n, -0) ? -1 : 1);
        }
        /**
         * Drives an initial value toward a target value linearly over a period of time.
         * @param initialValue The initial value.
         * @param targetValue The target value.
         * @param rate The rate at which to drive the value, in the same units as `dt`.
         * @param dt The amount of time over which to drive the value.
         * @param clampStart Whether to clamp the driven value such that it cannot be driven in the opposite direction from
         * the target value. Defaults to `true`.
         * @param clampEnd Whether to clamp the driven value such that it cannot be driven past the target value. Defaults to
         * `true`.
         * @returns The final driven value after the specified amount of time has elapsed.
         */
        static driveLinear(initialValue, targetValue, rate, dt, clampStart = true, clampEnd = true) {
            if (isNaN(initialValue) || isNaN(targetValue) || isNaN(rate) || isNaN(dt)) {
                return NaN;
            }
            if (rate === 0 || dt === 0 || initialValue === targetValue) {
                return initialValue;
            }
            if (targetValue - initialValue < 0) {
                return MathUtils.clamp(initialValue - rate * dt, clampEnd ? targetValue : -Infinity, clampStart ? initialValue : Infinity);
            }
            else {
                return MathUtils.clamp(initialValue + rate * dt, clampStart ? initialValue : -Infinity, clampEnd ? targetValue : Infinity);
            }
        }
        /**
         * Drives an initial value toward a target value via an exponential decay curve over a period of time.
         * @param initialValue The initial value.
         * @param targetValue The target value.
         * @param timeConstant The exponential decay time constant, in the same units as `dt`. Must be a positive number.
         * @param dt The amount of time over which to drive the value.
         * @returns The final driven value after the specified amount of time has elapsed.
         */
        static driveExp(initialValue, targetValue, timeConstant, dt) {
            return targetValue + (initialValue - targetValue) * Math.exp(-dt / timeConstant);
        }
    }
    /** Twice the value of pi. */
    MathUtils.TWO_PI = Math.PI * 2;
    /** Half the value of pi. */
    MathUtils.HALF_PI = Math.PI / 2;
    /** Square root of 3. */
    MathUtils.SQRT3 = Math.sqrt(3);
    /** Square root of 1/3. */
    MathUtils.SQRT1_3 = 1 / Math.sqrt(3);

    /**
     * Utility class for manipulating bit flags.
     */
    class BitFlags {
        /**
         * Generates a bit flag with a boolean value of true at a specified index.
         * @param index The index of the flag. Must be between 0 and 32, inclusive.
         * @returns a bit flag.
         * @throws Error if index is out of bounds.
         */
        static createFlag(index) {
            if (index < 0 || index > 32) {
                throw new Error(`Invalid index ${index} for bit flag. Index must be between 0 and 32.`);
            }
            return 1 << index;
        }
        /**
         * Gets the inverse of some bit flags.
         * @param flags The bit flag group containing the flags to invert.
         * @param mask An optional bit mask to use when applying the inverse operation. The operation will only be performed
         * at the indexes where the mask has a value of 1 (true). If a mask is not specified, the operation will be performed
         * at all indexes.
         * @returns the inverse
         */
        static not(flags, mask = ~0) {
            return flags ^ mask;
        }
        /**
         * Gets the union of zero or more bit flags.
         * @param flags A list of bit flags.
         * @returns the union of the bit flags.
         */
        static union(...flags) {
            let result = 0;
            const len = flags.length;
            for (let i = 0; i < len; i++) {
                result |= flags[i];
            }
            return result;
        }
        /**
         * Gets the intersection of zero or more bit flags.
         * @param flags A list of bit flags.
         * @returns the intersection of the bit flags.
         */
        static intersection(...flags) {
            const len = flags.length;
            if (len === 0) {
                return 0;
            }
            let result = flags[0];
            for (let i = 1; i < len; i++) {
                result &= flags[i];
            }
            return result;
        }
        /**
         * Changes a bit flag group by setting values at specific indexes.
         * @param flags The bit flag group to change.
         * @param valuesToSet A bit flag group containing the values to set.
         * @param mask A mask defining the indexes to set. Only indexes at which the mask has a value of `1` (`true`) will
         * be set.
         * @returns The result of changing `flags` using the specified values and indexes.
         */
        static set(flags, valuesToSet, mask) {
            return (flags & ~mask) | (valuesToSet & mask);
        }
        /**
         * Checks if a bit flag group meets at least one condition from a list of conditions.
         * @param flags A bit flag group.
         * @param conditions The conditions to meet, as a bit flag group.
         * @returns whether the bit flag group meets at least one condition.
         */
        static isAny(flags, conditions) {
            return (flags & conditions) !== 0;
        }
        /**
         * Checks if a bit flag group meets all the conditions from a list of conditions.
         * @param flags A bit flag group.
         * @param conditions The conditions to meet, as a bit flag group.
         * @returns whether the bit flag group meets all the conditions.
         */
        static isAll(flags, conditions) {
            return (flags & conditions) === conditions;
        }
        /**
         * Iterates through a bit flag group and executes a callback function once for each flag.
         * @param flags A bit flag group.
         * @param callback A function which will be called once for each flag.
         * @param valueFilter The value on which to filter. If defined, only flags with values equal to the filter will be
         * iterated, otherwise all flags will be iterated regardless of their values.
         * @param startIndex The index of the flag at which to start (inclusive). Defaults to 0.
         * @param endIndex The index of the flag at which to end (exclusive). Defaults to 32.
         */
        static forEach(flags, callback, valueFilter, startIndex, endIndex) {
            startIndex = Utils.Clamp(startIndex !== null && startIndex !== void 0 ? startIndex : (startIndex = 0), 0, 32);
            endIndex = Utils.Clamp(endIndex !== null && endIndex !== void 0 ? endIndex : (endIndex = 32), 0, 32);
            for (let i = startIndex; i < endIndex; i++) {
                const value = (flags & (1 << i)) !== 0;
                if (valueFilter === undefined || valueFilter === value) {
                    callback(value, i, flags);
                }
            }
        }
    }

    /**
     * Applies time-weighted exponential smoothing (i.e. an exponential moving average) to a sequence of raw values.
     *
     * When a new raw value is added to the sequence, it and the last smoothed value are weighted according to the time
     * elapsed since the last smoothed value was calculated (i.e. since the last raw value was added) and averaged. The
     * calculation of the weighting is such that the weight of each raw value in the sequence decays exponentially with the
     * "age" (i.e. time elapsed between when that value was added to the sequence and when the latest value was added to
     * the sequence) of the value.
     */
    class ExpSmoother {
        /**
         * Creates a new instance of ExpSmoother.
         * @param tau This smoother's time constant. The larger the constant, the greater the smoothing effect. A value less
         * than or equal to 0 is equivalent to no smoothing.
         * @param initial The initial smoothed value of this smoother. Defaults to null.
         * @param dtThreshold The elapsed time threshold, in seconds, above which this smoother will not smooth a new raw
         * value. Defaults to infinity.
         */
        constructor(tau, initial = null, dtThreshold = Infinity) {
            this.tau = tau;
            this.dtThreshold = dtThreshold;
            this.lastValue = initial;
        }
        /**
         * Gets the last smoothed value.
         * @returns The last smoothed value, or null if none exists.
         */
        last() {
            return this.lastValue;
        }
        /**
         * Adds a new raw value and gets the next smoothed value. If the new raw value is the first to be added since this
         * smoother was created or reset with no initial smoothed value, the returned smoothed value will be equal to the
         * raw value.
         * @param raw The new raw value.
         * @param dt The elapsed time since the last raw value was added.
         * @returns The next smoothed value.
         */
        next(raw, dt) {
            let next;
            if (this.tau > 0 && this.lastValue !== null) {
                const factor = this.calculateFactor(dt);
                next = ExpSmoother.smooth(raw, this.lastValue, factor);
            }
            else {
                next = raw;
            }
            this.lastValue = next;
            return next;
        }
        /**
         * Calculates the smoothing factor for a given time interval.
         * @param dt A time interval, in seconds.
         * @returns the smoothing factor for the given time interval.
         */
        calculateFactor(dt) {
            if (dt > this.dtThreshold) {
                return 0;
            }
            else {
                return Math.exp(-dt / this.tau);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        reset(value) {
            return this.lastValue = (value !== null && value !== void 0 ? value : null);
        }
        /**
         * Applies exponential smoothing.
         * @param value The value to smooth.
         * @param last The last smoothed value.
         * @param factor The smoothing factor.
         * @returns A smoothed value.
         */
        static smooth(value, last, factor) {
            return value * (1 - factor) + last * factor;
        }
    }

    /**
     * A calculator for lookahead values based on past trends. The calculator accepts a series of input values separated
     * by discrete time intervals, computes a rate of change at each time step, and uses this rate of change to predict
     * what the input value will be at some arbitrary length of time in the future assuming the rate of change remains
     * constant.
     */
    class Lookahead {
        /**
         * Constructor.
         * @param lookahead This calculator's lookahead time.
         * @param valueSmoothingTau The smoothing time constant to apply to the calculator's input values before rate of
         * change is computed. A value of `0` is equivalent to no smoothing. Defaults to `0`.
         * @param trendSmoothingTau The smoothing time constant to apply to the calculator's computed trend values. A value
         * of `0` is equivalent to no smoothing. Defaults to `0`.
         */
        constructor(lookahead, valueSmoothingTau = 0, trendSmoothingTau = 0) {
            this.lookahead = lookahead;
            this.lastSmoothedValue = null;
            this.lastTrendValue = 0;
            this.lastLookaheadValue = null;
            this.lastSmoothedLookaheadValue = null;
            this.valueSmoother = new ExpSmoother(valueSmoothingTau);
            this.trendSmoother = new ExpSmoother(trendSmoothingTau);
        }
        /**
         * Gets this calculator's last computed lookahead value. The lookahead value is the predicted value of this
         * calculator's input when projected into the future by an amount equal to the lookahead time assuming the current
         * rate of change of the input remains constant.
         * @param smoothed Whether to retrieve the lookahead value computed using the last smoothed input value instead of
         * the raw input value as the present (`t = 0`) value. Defaults to `false`.
         * @returns This calculator's last computed lookahead value.
         */
        last(smoothed = false) {
            return smoothed ? this.lastSmoothedLookaheadValue : this.lastLookaheadValue;
        }
        /**
         * Gets this calculator's last computed trend value. The trend value is the equal to the rate of change of this
         * calculator's input values multiplied by the lookahead time.
         * @returns This calculator's last computed trend value.
         */
        lastTrend() {
            return this.lastTrendValue;
        }
        /**
         * Adds a new input value and gets the next lookahead value. The lookahead value is the predicted value of this
         * calculator's input when projected into the future by an amount equal to the lookahead time assuming the current
         * rate of change of the input remains constant.
         * @param value The new input value.
         * @param dt The elapsed time since the last input value was added.
         * @param smoothed Whether to return the lookahead value computed using the smoothed input value instead of the raw
         * input value as the present (`t = 0`) value. Note that this argument does not determine whether smoothing is
         * applied for the purposes of calculating rate of change (smoothing is always applied for this purpose if a positive
         * time constant is defined). Defaults to `false`.
         * @returns The next lookahead value.
         */
        next(value, dt, smoothed = false) {
            const oldSmoothedValue = this.lastSmoothedValue;
            let trend;
            if (dt < 0) {
                return this.reset(value);
            }
            else if (dt > 0) {
                this.lastSmoothedValue = this.valueSmoother.next(value, dt);
                if (oldSmoothedValue === null) {
                    this.trendSmoother.reset();
                    trend = 0;
                }
                else {
                    trend = this.trendSmoother.next((this.lastSmoothedValue - oldSmoothedValue) / dt * this.lookahead, dt);
                }
            }
            else {
                trend = this.lastTrendValue;
                this.lastSmoothedValue = this.valueSmoother.next(value, dt);
            }
            this.lastTrendValue = trend;
            this.lastLookaheadValue = value + trend;
            this.lastSmoothedLookaheadValue = this.lastSmoothedValue + trend;
            return smoothed ? this.lastSmoothedLookaheadValue : this.lastLookaheadValue;
        }
        /**
         * Adds a new input value and gets the next trend value. The trend value is the equal to the rate of change of this
         * calculator's input values multiplied by the lookahead time.
         * @param value The new input value.
         * @param dt The elapsed time since the last input value was added.
         * @returns The next trend value.
         */
        nextTrend(value, dt) {
            this.next(value, dt);
            return this.lastTrendValue;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        reset(value) {
            this.lastSmoothedValue = this.valueSmoother.reset(value);
            this.trendSmoother.reset();
            this.lastTrendValue = 0;
            this.lastLookaheadValue = this.lastSmoothedValue;
            this.lastSmoothedLookaheadValue = this.lastSmoothedValue;
            return this.lastLookaheadValue;
        }
    }

    /**
     * Applies time-weighted exponential smoothing (i.e. an exponential moving average) to a sequence of raw values and
     * optionally uses smoothed estimates of velocity and acceleration to adjust the smooth value to compensate for trends
     * in the raw input values.
     *
     * When a new raw value is added to the sequence, it and the last smoothed value, with optional adjustments derived
     * from estimated velocity and acceleration, are weighted according to the time elapsed since the last smoothed value
     * was calculated (i.e. since the last raw value was added) and averaged. The calculation of the weighting is such that
     * the weight of each raw value in the sequence decays exponentially with the "age" (i.e. time elapsed between when
     * that value was added to the sequence and when the latest value was added to the sequence) of the value. Estimates of
     * velocity and acceleration are also smoothed in the same manner.
     */
    class MultiExpSmoother {
        /**
         * Creates a new instance of MultiExpSmoother.
         * @param tau This smoother's time constant. The larger the constant, the greater the smoothing effect. A value less
         * than or equal to 0 is equivalent to no smoothing.
         * @param tauVelocity This smoother's time constant for estimated velocity. The larger the constant, the greater the
         * smoothing effect applied to the estimated velocity. A value less than or equal to 0 is equivalent to no smoothing.
         * If not defined, then this smoother will not estimate velocity.
         * @param tauAccel This smoother's time constant for estimated acceleration. The larger the constant, the greater the
         * smoothing effect applied to the estimated acceleration. A value less than or equal to 0 is equivalent to no
         * smoothing. If this value or {@linkcode tauVelocity} is not defined, then this smoother will not estimate
         * acceleration.
         * @param initial The initial smoothed value of the smoother. Defaults to `null`.
         * @param initialVelocity The initial smoothed velocity estimate of the smoother. Ignored if {@linkcode tauVelocity}
         * is not defined. Defaults to `null`.
         * @param initialAccel The initial smoothed acceleration estimate of the smoother. Ignored if {@linkcode tauVelocity}
         * or {@linkcode tauAccel} is not defined. Defaults to `null`.
         * @param dtThreshold The elapsed time threshold, in seconds, above which this smoother will not smooth a new raw
         * value. Defaults to infinity.
         */
        constructor(tau, tauVelocity, tauAccel, initial = null, initialVelocity = null, initialAccel = null, dtThreshold = Infinity) {
            this.tau = tau;
            this.tauVelocity = tauVelocity;
            this.tauAccel = tauAccel;
            this.dtThreshold = dtThreshold;
            this.lastValue = this.lastRawValue = initial;
            this.lastVel = this.lastRawVel = initialVelocity;
            this.lastAccel = initialAccel;
        }
        /**
         * Gets the last smoothed value.
         * @returns The last smoothed value, or `null` if none exists.
         */
        last() {
            return this.lastValue;
        }
        /**
         * Gets the last smoothed velocity.
         * @returns The last smoothed velocity, or `null` if none exists.
         */
        lastVelocity() {
            return this.lastVel;
        }
        /**
         * Gets the last smoothed acceleration.
         * @returns The last smoothed acceleration, or `null` if none exists.
         */
        lastAcceleration() {
            return this.lastAccel;
        }
        /**
         * Adds a new raw value and gets the next smoothed value. If the new raw value is the first to be added since this
         * smoother was created or reset with no initial smoothed value, the returned smoothed value will be equal to the
         * raw value.
         * @param raw The new raw value.
         * @param dt The elapsed time since the last raw value was added.
         * @returns The next smoothed value.
         */
        next(raw, dt) {
            if (this.tau > 0 && this.lastValue !== null && this.lastRawValue !== null && dt <= this.dtThreshold) {
                const alpha = Math.exp(-dt / this.tau);
                const next = MultiExpSmoother.smooth(raw, this.lastValue + (this.lastVel === null ? 0 : this.lastVel * dt) + (this.lastAccel === null ? 0 : this.lastAccel * 0.5 * dt * dt), alpha);
                if (dt !== 0 && this.tauVelocity !== undefined) {
                    let nextVelocity;
                    const velocity = (raw - this.lastRawValue) / dt;
                    if (this.tauVelocity > 0 && this.lastVel !== null && this.lastRawVel !== null) {
                        const beta = Math.exp(-dt / this.tauVelocity);
                        nextVelocity = MultiExpSmoother.smooth(velocity, this.lastVel + (this.lastAccel === null ? 0 : this.lastAccel * dt), beta);
                        if (this.tauAccel !== undefined) {
                            const acceleration = (velocity - this.lastRawVel) / dt;
                            if (this.tauAccel > 0 && this.lastAccel !== null) {
                                const gamma = Math.exp(-dt / this.tauAccel);
                                this.lastAccel = MultiExpSmoother.smooth(acceleration, this.lastAccel, gamma);
                            }
                            else {
                                this.lastAccel = acceleration;
                            }
                        }
                    }
                    else {
                        nextVelocity = velocity;
                    }
                    this.lastRawVel = velocity;
                    this.lastVel = nextVelocity;
                }
                this.lastRawValue = raw;
                this.lastValue = next;
                return next;
            }
            else {
                return this.reset(raw);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        reset(value = null, velocity = null, accel = null) {
            this.lastVel = this.lastRawVel = velocity;
            this.lastAccel = accel;
            return this.lastValue = this.lastRawValue = value;
        }
        /**
         * Forecasts the smoothed value into the future based on the most recently calculated smoothed parameters (value,
         * velocity, and acceleration). If velocity or acceleration has not been calculated (or is not part of this
         * smoother's internal model), each will be treated as zero.
         * @param t The time in the future (relative to the last calculated smoothed value) at which to get the
         * forecasted value.
         * @returns The forecast smoothed value at the specified time, or `null` if a smoothed value has not yet been
         * calculated.
         */
        forecast(t) {
            if (this.lastValue === null) {
                return null;
            }
            else {
                return this.lastValue + (this.lastVel === null ? 0 : this.lastVel * t) + (this.lastAccel === null ? 0 : this.lastAccel * 0.5 * t * t);
            }
        }
        /**
         * Applies exponential smoothing.
         * @param value The value to smooth.
         * @param last The last smoothed value.
         * @param factor The smoothing factor.
         * @returns A smoothed value.
         */
        static smooth(value, last, factor) {
            return value * (1 - factor) + last * factor;
        }
    }

    /**
     * Utility methods for working with Subscribables.
     */
    class SubscribableUtils {
        /**
         * Checks if a query is a subscribable.
         * @param query A query.
         * @returns Whether the query is a subscribable.
         */
        static isSubscribable(query) {
            return typeof query === 'object' && query !== null && query.isSubscribable === true;
        }
        /**
         * Checks if a query is a mutable subscribable.
         * @param query A query.
         * @returns Whether the query is a mutable subscribable.
         */
        static isMutableSubscribable(query) {
            return typeof query === 'object' && query !== null && query.isMutableSubscribable === true;
        }
        /**
         * Checks if a query is a subscribable set.
         * @param query A query.
         * @returns Whether the query is a subscribable set.
         */
        static isSubscribableSet(query) {
            return typeof query === 'object' && query !== null && query.isSubscribableSet === true;
        }
        /**
         * Checks if a query is a mutable subscribable set.
         * @param query A query.
         * @returns Whether the query is a mutable subscribable set.
         */
        static isMutableSubscribableSet(query) {
            return typeof query === 'object' && query !== null && query.isMutableSubscribableSet === true;
        }
        /**
         * Converts a value to a subscribable.
         *
         * If the `excludeSubscribables` argument is `true` and the value is already a subscribable, then the value is
         * returned unchanged. Otherwise, a new subscribable whose state is always equal to the value will be created and
         * returned.
         * @param value The value to convert to a subscribable.
         * @param excludeSubscribables Whether to return subscribable values as-is instead of wrapping them in another
         * subscribable.
         * @returns A subscribable.
         */
        static toSubscribable(value, excludeSubscribables) {
            if (excludeSubscribables && SubscribableUtils.isSubscribable(value)) {
                return value;
            }
            else {
                return Subject.create(value);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static pipeMappedSource(source, to, sourceMap, arg4, arg5) {
            let pipeMap;
            let paused;
            if (typeof arg4 === 'function') {
                pipeMap = arg4;
                paused = arg5 !== null && arg5 !== void 0 ? arg5 : false;
            }
            else {
                paused = arg4 !== null && arg4 !== void 0 ? arg4 : false;
            }
            const sub = new MappedSourcePipe(source, to, undefined, sourceMap, pipeMap);
            if (!paused) {
                sub.resume(true);
            }
            return sub;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static pipeOptionalMappedSource(source, to, onOrphaned, sourceMap, arg5, arg6) {
            let pipeMap;
            let paused;
            if (typeof arg5 === 'function') {
                pipeMap = arg5;
                paused = arg6 !== null && arg6 !== void 0 ? arg6 : false;
            }
            else {
                paused = arg5 !== null && arg5 !== void 0 ? arg5 : false;
            }
            const sub = new MappedSourcePipe(source, to, onOrphaned, sourceMap, pipeMap);
            if (!paused) {
                sub.resume(true);
            }
            return sub;
        }
    }
    /**
     * A numeric equality function which returns `true` if and only if two numbers are strictly equal or if they are both
     * `NaN`.
     * @param a The first number to compare.
     * @param b The second number to compare.
     * @returns Whether the two numbers are strictly equal or both `NaN`.
     */
    SubscribableUtils.NUMERIC_NAN_EQUALITY = (a, b) => a === b || (isNaN(a) && isNaN(b));
    /**
     * An equality function which returns false unconditionally.
     * @returns false unconditionally.
     */
    SubscribableUtils.NEVER_EQUALITY = () => false;
    /**
     * A pipe from an input subscribable that is mapped from a source subscribable to an output mutable subscribable. Each
     * notification received by the pipe from the source subscribable is used to change the pipe's input subscribable.
     */
    class MappedSourcePipe {
        /** @inheritDoc */
        get isAlive() {
            return this._isAlive;
        }
        /** @inheritDoc */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Creates a new instance of MappedSourcePipe. The pipe is initialized as paused.
         * @param source The source of the pipe.
         * @param target The target of this pipe.
         * @param onOrphaned A function that selects a default value to use to set the state of the target subscribable
         * when the source state can't be mapped to a subscribable. The current value of the target subscribable is passed
         * to the function. The default value will _not_ be transformed by `pipeMap`. If not defined, then the target
         * subscribable is left unchanged when the source state can't be mapped to a subscribable.
         * @param sourceMap A function that maps source values to the pipe's input subscribables.
         * @param pipeMap A function that transforms this pipe's input values.
         */
        constructor(source, target, onOrphaned, sourceMap, pipeMap) {
            this.target = target;
            this.onOrphaned = onOrphaned;
            this.sourceMap = sourceMap;
            this.pipeMap = pipeMap;
            this._isAlive = true;
            this._isPaused = true;
            /** @inheritDoc */
            this.canInitialNotify = true;
            this.isPipePaused = true;
            this.sourceSub = source.sub(this.onSourceChanged.bind(this), false, true);
        }
        /**
         * Responds to when this pipe's source value changes.
         * @param source The new source value.
         */
        onSourceChanged(source) {
            var _a;
            (_a = this.pipe) === null || _a === void 0 ? void 0 : _a.destroy();
            const mappedSource = this.sourceMap(source);
            if (mappedSource) {
                if (this.pipeMap) {
                    this.pipe = mappedSource.pipe(this.target, this.pipeMap, this.isPipePaused);
                }
                else {
                    this.pipe = mappedSource.pipe(this.target, this.isPipePaused);
                }
            }
            else {
                this.pipe = undefined;
                if (!this.isPipePaused && this.onOrphaned) {
                    this.onOrphaned(this.target);
                }
            }
        }
        /** @inheritDoc */
        pause() {
            var _a;
            if (!this._isAlive) {
                throw new Error('Subscription: cannot pause a dead Subscription.');
            }
            this._isPaused = true;
            this.isPipePaused = true;
            this.sourceSub.pause();
            (_a = this.pipe) === null || _a === void 0 ? void 0 : _a.pause();
            return this;
        }
        /** @inheritDoc */
        resume(initialNotify = false) {
            if (!this._isAlive) {
                throw new Error('Subscription: cannot resume a dead Subscription.');
            }
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            this.sourceSub.resume(true);
            this.isPipePaused = false;
            if (this.pipe) {
                this.pipe.resume(initialNotify);
            }
            else if (initialNotify && this.onOrphaned) {
                this.onOrphaned(this.target);
            }
            return this;
        }
        /** @inheritDoc */
        destroy() {
            var _a;
            if (!this._isAlive) {
                return;
            }
            this._isAlive = false;
            this.sourceSub.destroy();
            (_a = this.pipe) === null || _a === void 0 ? void 0 : _a.destroy();
            this.pipe = undefined;
        }
    }

    /**
     * Utility class for generating common functions for mapping subscribables.
     */
    class SubscribableMapFunctions {
        /**
         * Generates a function which maps an input to itself.
         * @returns A function which maps an input to itself.
         */
        static identity() {
            return (input) => input;
        }
        /**
         * Generates a function which maps an input boolean to its negation.
         * @returns A function which maps an input boolean to its negation.
         */
        static not() {
            return (input) => !input;
        }
        /**
         * Generates a function which maps an input boolean tuple to `true` if at least one tuple member is `true` and to
         * `false` otherwise. A zero-length tuple is mapped to `false`.
         * @returns A function which maps an input boolean tuple to `true` if at least one tuple member is `true` and to
         * `false` otherwise.
         */
        static or() {
            return (input) => input.length > 0 && input.includes(true);
        }
        /**
         * Generates a function which maps an input boolean tuple to `true` if no tuple member is `true` and to
         * `false` otherwise. A zero-length tuple is mapped to `true`.
         * @returns A function which maps an input boolean tuple to `true` if no tuple member is `true` or there are no
         * tuple members, and to `false` otherwise.
         */
        static nor() {
            return (input) => !input.includes(true);
        }
        /**
         * Generates a function which maps an input boolean tuple to `true` if all tuple members are `true` and to `false`
         * otherwise. A zero-length tuple is mapped to `false`.
         * @returns A function which maps an input boolean tuple to `true` if all tuple members are `true` and to `false`
         * otherwise.
         */
        static and() {
            return (input) => input.length > 0 && !input.includes(false);
        }
        /**
         * Generates a function which maps an input boolean tuple to `false` if all tuple members are `true` and to `false`
         * otherwise. A zero-length tuple is mapped to `true`.
         * @returns A function which maps an input boolean tuple to `true` if all tuple members are `true` and to `false`
         * otherwise.
         */
        static nand() {
            return (input) => input.length < 1 || input.includes(false);
        }
        /**
         * Generates a function which maps an input number to its negation.
         * @returns A function which maps an input number to its negation.
         */
        static negate() {
            return (input) => -input;
        }
        /**
         * Generates a function which maps an input number to its absolute value.
         * @returns A function which maps an input number to its absolute value.
         */
        static abs() {
            return Math.abs;
        }
        /**
         * Generates a function which maps an input number tuple to the minimum numeric value contained in the tuple.
         * A zero-length tuple is mapped to Infinity.
         * @returns A function which maps an input number tuple to the minimum numeric value contained in the tuple.
         */
        static min() {
            return (input) => Math.min(...input);
        }
        /**
         * Generates a function which maps an input number tuple to the maximum numeric value contained in the tuple.
         * A zero-length tuple is mapped to -Infinity.
         * @returns A function which maps an input number tuple to the maximum numeric value contained in the tuple.
         */
        static max() {
            return (input) => Math.max(...input);
        }
        /**
         * Generates a function which maps an input tuple to a count of the number of items in the tuple that satisfy a
         * given condition.
         * @param predicate A function which evaluates whether an item should be counted.
         * @returns A function which maps an input tuple to a count of the number of items in the tuple that satisfy the
         * condition specified by the predicate.
         */
        static count(predicate) {
            const reduceFunc = (sum, curr) => {
                if (predicate(curr)) {
                    return sum + 1;
                }
                else {
                    return sum;
                }
            };
            return SubscribableMapFunctions.reduce(reduceFunc, 0);
        }
        /**
         * Generates a function which maps an input tuple to the sum of the numeric items contained in the tuple.
         * @returns A function which maps an input tuple to the sum of the numeric items contained in the tuple.
         */
        static sum() {
            const reduceFunc = (sum, curr) => sum + curr;
            return SubscribableMapFunctions.reduce(reduceFunc, 0);
        }
        /**
         * Generates a function which maps an input number tuple to the average numeric value contained in the tuple.
         * A zero-length tuple is mapped to NaN.
         * @returns A function which maps an input number tuple to the average numeric value contained in the tuple.
         */
        static average() {
            return (inputs) => {
                const inputLength = inputs.length;
                let sum = 0;
                for (let i = 0; i < inputLength; i++) {
                    sum += inputs[i];
                }
                return sum / inputLength;
            };
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static reduce(callbackFn, initialValue) {
            return (input) => input.reduce(callbackFn, initialValue);
        }
        /**
         * Generates a function which maps an input number to a rounded version of itself at a certain precision.
         * @param precision The precision to which to round the input. If the precision is defined as an `Accessible`, then
         * changes in the precision value will not be reflected in the mapped output until the next time the value of the
         * input number changes.
         * @param round The rounding behavior to use. Defaults to `Rounding.Nearest`.
         * @returns A function which maps an input number to a rounded version of itself at the specified precision.
         */
        static withPrecision(precision, round = exports.Rounding.Nearest) {
            const roundFunc = round > 0 ? MathUtils.ceil : round < 0 ? MathUtils.floor : MathUtils.round;
            return typeof precision === 'object'
                ? (input) => {
                    const precisionVal = precision.get();
                    return roundFunc(input, precisionVal);
                }
                : (input) => {
                    return roundFunc(input, precision);
                };
        }
        /**
         * Generates a function which maps an input number to a rounded version of itself at a certain precision with
         * hysteresis applied.
         *
         * When a previously mapped value exists, any new input value (`x`) is compared to the previously mapped value
         * (`y0`). Define `x1` as the least input value that can be rounded to `y0` and `x2` as the greatest input value that
         * can be rounded to `y0`. Then `x` is mapped to a new rounded output value if and only if `x < x1 - h1` or
         * `x > x2 + h2`, where `h1` and `h2` are the lower and upper hysteresis values, respectively. Otherwise, `x` is
         * mapped to `y0`.
         * @param precision The precision to which to round the input.
         * @param hysteresis The hysteresis to apply to the mapping function. If defined as a `[number, number]` tuple, then
         * the first number in the tuple is taken as the lower hysteresis and second number as the upper hysteresis. If
         * defined as a single number, then that is taken as both the lower and upper hysteresis. Negative values are clamped
         * to zero.
         * @param round The rounding behavior to use. Defaults to `Rounding.Nearest`.
         * @returns A function which maps an input number to a rounded version of itself at the specified precision and with
         * the specified hysteresis.
         */
        static withPrecisionHysteresis(precision, hysteresis, round = exports.Rounding.Nearest) {
            let hysteresisLower;
            let hysteresisUpper;
            if (typeof hysteresis === 'number') {
                hysteresisLower = hysteresisUpper = Math.max(0, hysteresis);
            }
            else {
                hysteresisLower = Math.max(0, hysteresis[0]);
                hysteresisUpper = Math.max(0, hysteresis[1]);
            }
            let roundFunc;
            let lowerOffset;
            let upperOffset;
            if (round > 0) {
                roundFunc = MathUtils.ceil;
                lowerOffset = -(precision + hysteresisLower);
                upperOffset = hysteresisUpper;
            }
            else if (round < 0) {
                roundFunc = MathUtils.floor;
                lowerOffset = -hysteresisLower;
                upperOffset = precision + hysteresisUpper;
            }
            else {
                roundFunc = MathUtils.round;
                lowerOffset = -(precision * 0.5 + hysteresisLower);
                upperOffset = precision * 0.5 + hysteresisUpper;
            }
            // Use a helper function to generate the mapping function so that the generated closure doesn't capture extraneous
            // variables and prevent them from being GC'd.
            return SubscribableMapFunctions.withPrecisionHysteresisHelper(precision, roundFunc, lowerOffset, upperOffset, round <= 0);
        }
        /**
         * Generates a function which maps an input number to a rounded version of itself at a certain precision with
         * hysteresis applied.
         * @param precision The precision to which to round the input.
         * @param roundFunc The function to use to round the input.
         * @param lowerOffset The offset from the previously mapped value to the lower boundary of the range in which a new
         * input number should not be mapped to a new output value.
         * @param upperOffset The offset from the previously mapped value to the upper boundary of the range in which a new
         * input number should not be mapped to a new output value.
         * @param isRangeOpenLower Whether the boundaries of the range in which a new input number should not be mapped to a
         * new output value is open at the lower end and closed at the upper end instead of closed at the lower end and open
         * at the upper end.
         * @returns A function which maps an input number to a rounded version of itself at the specified precision and with
         * the specified hysteresis.
         */
        static withPrecisionHysteresisHelper(precision, roundFunc, lowerOffset, upperOffset, isRangeOpenLower) {
            if (isRangeOpenLower) {
                return (input, currentVal) => {
                    if (currentVal === undefined || !isFinite(input)) {
                        return roundFunc(input, precision);
                    }
                    else {
                        if (input < currentVal + lowerOffset || input >= currentVal + upperOffset) {
                            return roundFunc(input, precision);
                        }
                        else {
                            return currentVal;
                        }
                    }
                };
            }
            else {
                return (input, currentVal) => {
                    if (currentVal === undefined || !isFinite(input)) {
                        return roundFunc(input, precision);
                    }
                    else {
                        if (input <= currentVal + lowerOffset || input > currentVal + upperOffset) {
                            return roundFunc(input, precision);
                        }
                        else {
                            return currentVal;
                        }
                    }
                };
            }
        }
        /**
         * Generates a function which maps an input number to itself if and only if it differs from the previous mapped value
         * by a certain amount, and to the previous mapped value otherwise.
         * @param threshold The minimum difference between the input and the previous mapped value required to map the input
         * to itself.
         * @returns A function which maps an input number to itself if and only if it differs from the previous mapped value
         * by the specified amount, and to the previous mapped value otherwise.
         */
        static changedBy(threshold) {
            return SubscribableUtils.isSubscribable(threshold)
                ? (input, currentVal) => currentVal === undefined || Math.abs(input - currentVal) >= threshold.get() ? input : currentVal
                : (input, currentVal) => currentVal === undefined || Math.abs(input - currentVal) >= threshold ? input : currentVal;
        }
    }

    /**
     * A subscribable subject that is a mapped stream from one or more input subscribables.
     */
    class MappedSubject extends AbstractSubscribable {
        /** @inheritdoc */
        get isAlive() {
            return this._isAlive;
        }
        /** @inheritdoc */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Creates a new MappedSubject.
         * @param mapFunc The function which maps this subject's inputs to a value.
         * @param equalityFunc The function which this subject uses to check for equality between values.
         * @param mutateFunc The function which this subject uses to change its value.
         * @param initialVal The initial value of this subject.
         * @param inputs The subscribables which provide the inputs to this subject.
         */
        constructor(mapFunc, equalityFunc, mutateFunc, initialVal, ...inputs) {
            super();
            this.mapFunc = mapFunc;
            this.equalityFunc = equalityFunc;
            /** @inheritdoc */
            this.canInitialNotify = true;
            this._isAlive = true;
            this._isPaused = false;
            this.inputs = inputs;
            this.inputValues = inputs.map(input => input.get());
            if (initialVal && mutateFunc) {
                this.value = initialVal;
                mutateFunc(this.value, this.mapFunc(this.inputValues, undefined));
                this.mutateFunc = (newVal) => { mutateFunc(this.value, newVal); };
            }
            else {
                this.value = this.mapFunc(this.inputValues, undefined);
                this.mutateFunc = (newVal) => { this.value = newVal; };
            }
            this.inputSubs = this.inputs.map((input, index) => input.sub(inputValue => {
                this.inputValues[index] = inputValue;
                this.updateValue();
            }));
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(...args) {
            let mapFunc, equalityFunc, mutateFunc, initialVal;
            if (typeof args[0] === 'function') {
                // Mapping function was supplied.
                mapFunc = args.shift();
                if (typeof args[0] === 'function') {
                    equalityFunc = args.shift();
                }
                else {
                    equalityFunc = AbstractSubscribable.DEFAULT_EQUALITY_FUNC;
                }
                if (typeof args[0] === 'function') {
                    mutateFunc = args.shift();
                    initialVal = args.shift();
                }
            }
            else {
                mapFunc = MappedSubject.IDENTITY_MAP;
                equalityFunc = MappedSubject.NEVER_EQUALS;
            }
            return new MappedSubject(mapFunc, equalityFunc, mutateFunc, initialVal, ...args);
        }
        /**
         * Re-maps this subject's value from its input, and notifies subscribers if this results in a change to the mapped
         * value according to this subject's equality function.
         */
        updateValue() {
            const value = this.mapFunc(this.inputValues, this.value);
            if (!this.equalityFunc(this.value, value)) {
                this.mutateFunc(value);
                this.notify();
            }
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        /** @inheritdoc */
        pause() {
            if (!this._isAlive) {
                throw new Error('MappedSubject: cannot pause a dead subject');
            }
            if (this._isPaused) {
                return this;
            }
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputSubs[i].pause();
            }
            this._isPaused = true;
            return this;
        }
        /** @inheritdoc */
        resume() {
            if (!this._isAlive) {
                throw new Error('MappedSubject: cannot resume a dead subject');
            }
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputValues[i] = this.inputs[i].get();
                this.inputSubs[i].resume();
            }
            this.updateValue();
            return this;
        }
        /** @inheritdoc */
        destroy() {
            this._isAlive = false;
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputSubs[i].destroy();
            }
        }
    }
    MappedSubject.IDENTITY_MAP = SubscribableMapFunctions.identity();
    MappedSubject.NEVER_EQUALS = () => false;

    /**
     * A Subject which provides a {@link NumberUnitInterface} value.
     */
    class NumberUnitSubject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         */
        constructor(value) {
            super();
            this.value = value;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a NumberUnitSubject.
         * @param initialVal The initial value.
         * @returns A NumberUnitSubject.
         */
        static create(initialVal) {
            return new NumberUnitSubject(initialVal);
        }
        /**
         * Creates a NumberUnitSubject.
         * @param initialVal The initial value.
         * @returns A NumberUnitSubject.
         * @deprecated Use `NumberUnitSubject.create()` instead.
         */
        static createFromNumberUnit(initialVal) {
            return new NumberUnitSubject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value.readonly;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2) {
            const isArg1Number = typeof arg1 === 'number';
            const equals = isArg1Number ? this.value.equals(arg1, arg2) : this.value.equals(arg1);
            if (!equals) {
                isArg1Number ? this.value.set(arg1, arg2) : this.value.set(arg1);
                this.notify();
            }
        }
        /**
         * Returns a number {@link Subscribable} of this `NumberUnit` converted according to a unit or unit `Subscribable`
         *
         * @param unit the unit to subscribe to
         *
         * @returns a `MappedSubscribable<number>`
         */
        asUnit(unit) {
            return MappedSubject.create(([numberUnit, toUnit]) => numberUnit.asUnit(toUnit), this, SubscribableUtils.toSubscribable(unit, true));
        }
    }

    /**
     * A utitlity class for calculating a numerical average of a selected number of samples.
     */
    class SimpleMovingAverage {
        /**
         * Class to return a numerical average from a specified number of inputs.
         * @param samples is the number of samples.
         */
        constructor(samples) {
            this.samples = samples;
            this._values = [];
        }
        /**
         * Returns a numerical average of the inputs.
         * @param input is the input number.
         * @returns The numerical average.
         */
        getAverage(input) {
            if (this._values.length === this.samples) {
                this._values.splice(0, 1);
            }
            this._values.push(input);
            let sum = 0;
            this._values.forEach((v) => {
                sum += v;
            });
            return sum / this._values.length;
        }
        /**
         * Resets the average.
         */
        reset() {
            this._values = [];
        }
    }

    /**
     * A closed heat system with temperature sources contributing to a volume.
     */
    class TemperatureSystem {
        /**
         * Gets the system's output value.
         * @returns The system's output temperature value, in degrees Celsius.
         */
        get value() {
            return this._value;
        }
        /**
         * Creates an instance of a TemperatureSystem.
         * @param capacity The heat capacity of the system, in joules per kelvin.
         */
        constructor(capacity) {
            this.capacity = capacity;
            this.sources = [];
            this._value = Subject.create(0);
        }
        /**
         * Directly sets the current temperature value.
         * @param value The temperature value, in degrees Celsius.
         */
        set(value) {
            this._value.set(Math.max(-273.15, value));
        }
        /**
         * Adds a temperature source.
         * @param source The temperature source.
         * @returns The index of the added temperature source.
         */
        addSource(source) {
            return this.sources.push(source) - 1;
        }
        /**
         * Sets the temperature of a temperature source.
         * @param index The index of the source.
         * @param temperature The temperature to set to, in degrees Celsius.
         */
        setSourceTemp(index, temperature) {
            const source = this.sources[index];
            if (source !== undefined) {
                source.temperature = temperature;
            }
        }
        /**
         * Sets the conductivity of a temperature source.
         * @param index The index of the source.
         * @param conductivity The conductivity to set to, in watts per meter-kelvin.
         */
        setSourceConductivity(index, conductivity) {
            const source = this.sources[index];
            if (source !== undefined) {
                source.conductivity = conductivity;
            }
        }
        /**
         * Sets the heat capacity of the system.
         * @param capacity The heat capacity of the system, in joules per kelvin.
         */
        setCapacity(capacity) {
            this.capacity = capacity;
        }
        /**
         * Updates the temperature of this system following a period of elapsed time. This method assumes that this system's
         * sources, their temperatures and conductivities, and this system's heat capacity at the time this method is called
         * all remained constant throughout the time elapsed.
         * @param deltaTime The elapsed time, in milliseconds.
         */
        update(deltaTime) {
            // If no time has elapsed, or if there are no heat sources/sinks, then the system temperature cannot change.
            if (deltaTime === 0 || this.sources.length === 0) {
                return;
            }
            // For each time step, assuming the temperature of the sources, their conductivities, and the system heat capacity
            // all remain constant, the total heat of the system, Q, is governed by the equation
            // dQ/dt = A + k_s * Q(t) / C
            // 
            // A = T_1 * k_1 + T_2 * k_2 + ... T_n * k_n,
            //   where T_i, k_i are the absolute temperature and thermal conductivity, respectively, of source i
            //
            // k_s = k_1 + k_2 + ... + k_n
            //
            // C is the system's heat capacity
            //
            // Solving the first-order DE above yields
            // Q(t) = A * C / k_s + (Q(0) - A * C / k_s) * e ^ (-k_s * t / C)
            const Q0 = (this._value.get() + 273.15) * this.capacity;
            let A = 0, k = 0;
            for (let i = 0; i < this.sources.length; i++) {
                const source = this.sources[i];
                A += Math.max(source.temperature + 273.15, 0) * source.conductivity;
                k += source.conductivity;
            }
            let Q;
            if (k === 0) {
                // If k_s equals 0, then the DE above simplifies to
                // dQ/dt = A
                //
                // Solving for Q(t) yields
                // Q(t) = Q(0) + A * t
                Q = Q0 + A * deltaTime / 1000;
            }
            else {
                const B = A * this.capacity / k;
                Q = B + (Q0 - B) * Math.exp(-k * deltaTime / 1000 / this.capacity);
            }
            this._value.set(Math.max(Q, 0) / this.capacity - 273.15);
        }
    }

    /**
     * 2D vector mathematical operations.
     */
    class Vec2Math {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(x, y) {
            const vec = new Float64Array(2);
            if (x !== undefined && y !== undefined) {
                vec[0] = x;
                vec[1] = y;
            }
            return vec;
        }
        /**
         * Gets the polar angle theta of a vector in radians.
         * @param vec - a vector.
         * @returns the polar angle theta of the vector.
         */
        static theta(vec) {
            return Math.atan2(vec[1], vec[0]);
        }
        /**
         * Sets the components of a vector.
         * @param x - the new x-component.
         * @param y - the new y-component.
         * @param vec - the vector to change.
         * @returns the vector after it has been changed.
         */
        static set(x, y, vec) {
            vec[0] = x;
            vec[1] = y;
            return vec;
        }
        /**
         * Sets the polar components of a vector.
         * @param r - the new length (magnitude).
         * @param theta - the new polar angle theta, in radians.
         * @param vec - the vector to change.
         * @returns the vector after it has been changed.
         */
        static setFromPolar(r, theta, vec) {
            vec[0] = r * Math.cos(theta);
            vec[1] = r * Math.sin(theta);
            return vec;
        }
        /**
         * Add one vector to another.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @param out The vector to write the results to.
         * @returns the vector sum.
         */
        static add(v1, v2, out) {
            out[0] = v1[0] + v2[0];
            out[1] = v1[1] + v2[1];
            return out;
        }
        /**
         * Subtracts one vector from another.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @param out The vector to write the results to.
         * @returns the vector difference.
         */
        static sub(v1, v2, out) {
            out[0] = v1[0] - v2[0];
            out[1] = v1[1] - v2[1];
            return out;
        }
        /**
         * Gets the dot product of two vectors.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @returns The dot product of the vectors.
         */
        static dot(v1, v2) {
            return v1[0] * v2[0] + v1[1] * v2[1];
        }
        /**
         * Gets the determinant of two vectors.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @returns The determinant of the vectors.
         */
        static det(v1, v2) {
            return v1[0] * v2[1] - v1[1] * v2[0];
        }
        /**
         * Multiplies a vector by a scalar.
         * @param v1 The vector to multiply.
         * @param scalar The scalar to apply.
         * @param out The vector to write the results to.
         * @returns The scaled vector.
         */
        static multScalar(v1, scalar, out) {
            out[0] = v1[0] * scalar;
            out[1] = v1[1] * scalar;
            return out;
        }
        /**
         * Gets the magnitude of a vector.
         * @param v1 The vector to get the magnitude for.
         * @returns the vector's magnitude.
         */
        static abs(v1) {
            return Math.hypot(v1[0], v1[1]);
        }
        /**
         * Normalizes the vector to a unit vector.
         * @param v1 The vector to normalize.
         * @param out The vector to write the results to.
         * @returns the normalized vector.
         */
        static normalize(v1, out) {
            const mag = Vec2Math.abs(v1);
            out[0] = v1[0] / mag;
            out[1] = v1[1] / mag;
            return out;
        }
        /**
         * Gets the normal of the supplied vector.
         * @param v1 The vector to get the normal for.
         * @param out The vector to write the results to.
         * @param counterClockwise Whether or not to get the counterclockwise normal.
         * @returns the normal vector.
         */
        static normal(v1, out, counterClockwise = false) {
            const x = v1[0];
            const y = v1[1];
            if (!counterClockwise) {
                out[0] = y;
                out[1] = -x;
            }
            else {
                out[0] = -y;
                out[1] = x;
            }
            return out;
        }
        /**
         * Gets the Euclidean distance between two vectors.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns the Euclidean distance between the two vectors.
         */
        static distance(vec1, vec2) {
            return Math.hypot(vec2[0] - vec1[0], vec2[1] - vec1[1]);
        }
        /**
         * Gets the angle between two vectors, in radians.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns The angle between the two specified unit vectors, in radians, or `NaN` if either of the vectors has a
         * magnitude equal to zero.
         */
        static angle(vec1, vec2) {
            const absProduct = Vec2Math.abs(vec1) * Vec2Math.abs(vec2);
            if (absProduct === 0) {
                return NaN;
            }
            else {
                return Vec2Math.unitAngle(vec1, vec2) / absProduct;
            }
        }
        /**
         * Gets the angle between two unit vectors, in radians.
         * @param vec1 The first unit vector.
         * @param vec2 The second unit vector.
         * @returns The angle between the two specified unit vectors, in radians.
         */
        static unitAngle(vec1, vec2) {
            return Math.acos(MathUtils.clamp(Vec2Math.dot(vec1, vec2), -1, 1));
        }
        /**
         * Checks if two vectors are equal.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns Whether the two vectors are equal.
         */
        static equals(vec1, vec2) {
            return vec1[0] === vec2[0] && vec1[1] === vec2[1];
        }
        /**
         * Checks if a vector is finite. A vector is considered finite if all of its components are finite.
         * @param vec The vector to check.
         * @returns Whether the specified vector is finite.
         */
        static isFinite(vec) {
            return isFinite(vec[0]) && isFinite(vec[1]);
        }
        /**
         * Copies one vector to another.
         * @param from The vector from which to copy.
         * @param to The vector to which to copy.
         * @returns The changed vector.
         */
        static copy(from, to) {
            return Vec2Math.set(from[0], from[1], to);
        }
        /**
         * Checks if a point is within a polygon.
         * @param polygon The polygon to check against.
         * @param point The point to test.
         * @returns True if the point is within or on the polygon, false otherwise.
         * @throws An error if first and last points in a polygon are not the same.
         */
        static pointWithinPolygon(polygon, point) {
            //Adapted from https://github.com/rowanwins/point-in-polygon-hao
            let k = 0;
            let f = 0;
            let u1 = 0;
            let v1 = 0;
            let u2 = 0;
            let v2 = 0;
            let currentP = null;
            let nextP = null;
            const x = point[0];
            const y = point[1];
            const contourLen = polygon.length - 1;
            currentP = polygon[0];
            if (currentP[0] !== polygon[contourLen][0] &&
                currentP[1] !== polygon[contourLen][1]) {
                throw new Error('First and last coordinates in a ring must be the same');
            }
            u1 = currentP[0] - x;
            v1 = currentP[1] - y;
            for (let i = 0; i < polygon.length - 1; i++) {
                nextP = polygon[i + 1];
                v2 = nextP[1] - y;
                if ((v1 < 0 && v2 < 0) || (v1 > 0 && v2 > 0)) {
                    currentP = nextP;
                    v1 = v2;
                    u1 = currentP[0] - x;
                    continue;
                }
                u2 = nextP[0] - point[0];
                if (v2 > 0 && v1 <= 0) {
                    f = (u1 * v2) - (u2 * v1);
                    if (f > 0) {
                        k = k + 1;
                    }
                    else if (f === 0) {
                        return undefined;
                    }
                }
                else if (v1 > 0 && v2 <= 0) {
                    f = (u1 * v2) - (u2 * v1);
                    if (f < 0) {
                        k = k + 1;
                    }
                    else if (f === 0) {
                        return undefined;
                    }
                }
                else if (v2 === 0 && v1 < 0) {
                    f = (u1 * v2) - (u2 * v1);
                    if (f === 0) {
                        return undefined;
                    }
                }
                else if (v1 === 0 && v2 < 0) {
                    f = u1 * v2 - u2 * v1;
                    if (f === 0) {
                        return undefined;
                    }
                }
                else if (v1 === 0 && v2 === 0) {
                    if (u2 <= 0 && u1 >= 0) {
                        return undefined;
                    }
                    else if (u1 <= 0 && u2 >= 0) {
                        return undefined;
                    }
                }
                currentP = nextP;
                v1 = v2;
                u1 = u2;
            }
            if (k % 2 === 0) {
                return false;
            }
            return true;
        }
    }
    /**
     * 3D vector mathematical operations.
     */
    class Vec3Math {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(x, y, z) {
            const vec = new Float64Array(3);
            if (x !== undefined && y !== undefined && z !== undefined) {
                vec[0] = x;
                vec[1] = y;
                vec[2] = z;
            }
            return vec;
        }
        /**
         * Gets the spherical angle theta (polar angle) of a vector in radians.
         * @param vec A vector.
         * @returns The spherical angle theta of the vector.
         */
        static theta(vec) {
            return Math.atan2(Math.hypot(vec[0], vec[1]), vec[2]);
        }
        /**
         * Gets the spherical angle phi (azimuthal angle) of a vector in radians.
         * @param vec A vector.
         * @returns The spherical angle phi of the vector.
         */
        static phi(vec) {
            return Math.atan2(vec[1], vec[0]);
        }
        /**
         * Sets the components of a vector.
         * @param x The new x-component.
         * @param y The new y-component.
         * @param z The new z-component.
         * @param vec The vector to change.
         * @returns The vector after it has been changed.
         */
        static set(x, y, z, vec) {
            vec[0] = x;
            vec[1] = y;
            vec[2] = z;
            return vec;
        }
        /**
         * Sets the spherical components of a vector.
         * @param r The new length (magnitude).
         * @param theta The new spherical angle theta (polar angle), in radians.
         * @param phi The new spherical angle phi (azimuthal angle), in radians.
         * @param vec The vector to change.
         * @returns The vector after it has been changed.
         */
        static setFromSpherical(r, theta, phi, vec) {
            const sinTheta = Math.sin(theta);
            vec[0] = r * sinTheta * Math.cos(phi);
            vec[1] = r * sinTheta * Math.sin(phi);
            vec[2] = r * Math.cos(theta);
            return vec;
        }
        /**
         * Add one vector to another.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @param out The vector to write the results to.
         * @returns the vector sum.
         */
        static add(v1, v2, out) {
            out[0] = v1[0] + v2[0];
            out[1] = v1[1] + v2[1];
            out[2] = v1[2] + v2[2];
            return out;
        }
        /**
         * Subtracts one vector from another.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @param out The vector to write the results to.
         * @returns the vector difference.
         */
        static sub(v1, v2, out) {
            out[0] = v1[0] - v2[0];
            out[1] = v1[1] - v2[1];
            out[2] = v1[2] - v2[2];
            return out;
        }
        /**
         * Gets the dot product of two vectors.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @returns The dot product of the vectors.
         */
        static dot(v1, v2) {
            return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
        }
        /**
         * Gets the cross product of two vectors.
         * @param v1 - the first vector.
         * @param v2 - the second vector.
         * @param out - the vector to which to write the result.
         * @returns the cross product.
         */
        static cross(v1, v2, out) {
            const x1 = v1[0];
            const y1 = v1[1];
            const z1 = v1[2];
            const x2 = v2[0];
            const y2 = v2[1];
            const z2 = v2[2];
            out[0] = y1 * z2 - z1 * y2;
            out[1] = z1 * x2 - x1 * z2;
            out[2] = x1 * y2 - y1 * x2;
            return out;
        }
        /**
         * Multiplies a vector by a scalar.
         * @param v1 The vector to multiply.
         * @param scalar The scalar to apply.
         * @param out The vector to write the results to.
         * @returns The scaled vector.
         */
        static multScalar(v1, scalar, out) {
            out[0] = v1[0] * scalar;
            out[1] = v1[1] * scalar;
            out[2] = v1[2] * scalar;
            return out;
        }
        /**
         * Gets the magnitude of a vector.
         * @param v1 The vector to get the magnitude for.
         * @returns the vector's magnitude.
         */
        static abs(v1) {
            return Math.hypot(v1[0], v1[1], v1[2]);
        }
        /**
         * Sets the magnitude of a vector.
         * @param v1 The vector to receive a new length.
         * @param magnitude The length to apply.
         * @param out The vector to write the results to.
         * @returns The scaled vector.
         */
        static setMagnitude(v1, magnitude, out) {
            const magnitudeV1 = Vec3Math.abs(v1);
            const factor = (magnitudeV1 === 0) ? NaN : magnitude / magnitudeV1;
            out[0] = factor * v1[0];
            out[1] = factor * v1[1];
            out[2] = factor * v1[2];
            return out;
        }
        /**
         * Normalizes the vector to a unit vector.
         * @param v1 The vector to normalize.
         * @param out The vector to write the results to.
         * @returns the normalized vector.
         */
        static normalize(v1, out) {
            const mag = Vec3Math.abs(v1);
            out[0] = v1[0] / mag;
            out[1] = v1[1] / mag;
            out[2] = v1[2] / mag;
            return out;
        }
        /**
         * Gets the Euclidean distance between two vectors.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns the Euclidean distance between the two vectors.
         */
        static distance(vec1, vec2) {
            return Math.hypot(vec2[0] - vec1[0], vec2[1] - vec1[0], vec2[2] - vec1[2]);
        }
        /**
         * Gets the angle between two vectors, in radians.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns The angle between the two specified unit vectors, in radians, or `NaN` if either of the vectors has a
         * magnitude equal to zero.
         */
        static angle(vec1, vec2) {
            const absProduct = Vec3Math.abs(vec1) * Vec3Math.abs(vec2);
            if (absProduct === 0) {
                return NaN;
            }
            else {
                return Vec3Math.unitAngle(vec1, vec2) / absProduct;
            }
        }
        /**
         * Gets the angle between two unit vectors, in radians.
         * @param vec1 The first unit vector.
         * @param vec2 The second unit vector.
         * @returns The angle between the two specified unit vectors, in radians.
         */
        static unitAngle(vec1, vec2) {
            return Math.acos(MathUtils.clamp(Vec3Math.dot(vec1, vec2), -1, 1));
        }
        /**
         * Checks if two vectors are equal.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns Whether the two vectors are equal.
         */
        static equals(vec1, vec2) {
            return vec1[0] === vec2[0] && vec1[1] === vec2[1] && vec1[2] === vec2[2];
        }
        /**
         * Checks if a vector is finite. A vector is considered finite if all of its components are finite.
         * @param vec The vector to check.
         * @returns Whether the specified vector is finite.
         */
        static isFinite(vec) {
            return isFinite(vec[0]) && isFinite(vec[1]) && isFinite(vec[2]);
        }
        /**
         * Copies one vector to another.
         * @param from The vector from which to copy.
         * @param to The vector to which to copy.
         * @returns the changed vector.
         */
        static copy(from, to) {
            return Vec3Math.set(from[0], from[1], from[2], to);
        }
    }
    /**
     * N-dimensional vector mathematical operations.
     */
    class VecNMath {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(length, ...components) {
            const vec = new Float64Array(length);
            for (let i = 0; i < length && components.length; i++) {
                vec[i] = components[i];
            }
            return vec;
        }
        /**
         * Sets the components of a vector.
         * @param vec The vector to change.
         * @param components The new components.
         * @returns The vector after it has been changed.
         */
        static set(vec, ...components) {
            for (let i = 0; i < vec.length && components.length; i++) {
                vec[i] = components[i];
            }
            return vec;
        }
        /**
         * Gets the magnitude of a vector.
         * @param vec The vector to get the magnitude for.
         * @returns The vector's magnitude.
         */
        static abs(vec) {
            return Math.hypot(...vec);
        }
        /**
         * Gets the dot product of two vectors.
         * @param v1 The first vector.
         * @param v2 The second vector.
         * @returns The dot product of the vectors.
         * @throws Error if the two vectors are of unequal lengths.
         */
        static dot(v1, v2) {
            if (v1.length !== v2.length) {
                throw new Error(`VecNMath: cannot compute dot product of two vectors of unequal length (${v1.length} and ${v2.length})`);
            }
            let dot = 0;
            const len = v1.length;
            for (let i = 0; i < len; i++) {
                dot += v1[i] * v2[i];
            }
            return dot;
        }
        /**
         * Normalizes a vector to a unit vector.
         * @param v1 The vector to normalize.
         * @param out The vector to write the results to.
         * @returns The normalized vector.
         */
        static normalize(v1, out) {
            const mag = Vec3Math.abs(v1);
            const len = v1.length;
            for (let i = 0; i < len; i++) {
                out[i] = v1[i] / mag;
            }
            return out;
        }
        /**
         * Checks if two vectors are equal.
         * @param vec1 The first vector.
         * @param vec2 The second vector.
         * @returns Whether the two vectors are equal.
         */
        static equals(vec1, vec2) {
            if (vec1.length !== vec2.length) {
                return false;
            }
            for (let i = 0; i < vec1.length; i++) {
                if (vec1[i] !== vec2[i]) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Checks if a vector is finite. A vector is considered finite if all of its components are finite.
         * @param vec The vector to check.
         * @returns Whether the specified vector is finite.
         */
        static isFinite(vec) {
            for (let i = 0; i < vec.length; i++) {
                if (!isFinite(vec[i])) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Copies one vector to another.
         * @param from The vector from which to copy.
         * @param to The vector to which to copy.
         * @returns The changed vector.
         * @throws Error if the vectors are of unequal lengths.
         */
        static copy(from, to) {
            if (from.length !== to.length) {
                throw new Error(`VecNMath: cannot copy a vector of length ${from.length} to a vector of length ${to.length}`);
            }
            to.set(from);
            return to;
        }
    }

    /**
     * A 2D affine transformation. By default, Transform2D objects are initially created as identity transformations.
     */
    class Transform2D {
        constructor() {
            this.array = new Float64Array([1, 0, 0, 0, 1, 0]);
        }
        /**
         * Gets the parameters of this transformation as a 6-tuple: `[scaleX, skewX, translateX, skewY, scaleY, translateY]`.
         * @returns The parameters of this transformation.
         */
        getParameters() {
            return this.array;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, skewX, translateX, skewY, scaleY, translateY) {
            let scaleX = arg1;
            if (arg1 instanceof Transform2D) {
                [scaleX, skewX, translateX, skewY, scaleY, translateY] = arg1.array;
            }
            const array = this.array;
            array[0] = scaleX;
            array[1] = skewX;
            array[2] = translateX;
            array[3] = skewY;
            array[4] = scaleY;
            array[5] = translateY;
            return this;
        }
        /**
         * Sets the x scaling factor of this transformation.
         * @param value The new x scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScaleX(value) {
            this.array[0] = value;
            return this;
        }
        /**
         * Sets the y scaling factor of this transformation.
         * @param value The new y scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScaleY(value) {
            this.array[4] = value;
            return this;
        }
        /**
         * Sets the x and y scaling factors of this transformation.
         * @param x The new x scaling factor.
         * @param y The new y scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScale(x, y) {
            this.array[0] = x;
            this.array[4] = y;
            return this;
        }
        /**
         * Sets the x skew factor of this transformation.
         * @param value The new x skew factor.
         * @returns This transformation, after it has been changed.
         */
        setSkewX(value) {
            this.array[1] = value;
            return this;
        }
        /**
         * Sets the y skew factor of this transformation.
         * @param value The new y skew factor.
         * @returns This transformation, after it has been changed.
         */
        setSkewY(value) {
            this.array[3] = value;
            return this;
        }
        /**
         * Sets the x translation of this transformation.
         * @param value The new x translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslateX(value) {
            this.array[2] = value;
            return this;
        }
        /**
         * Sets the y translation of this transformation.
         * @param value The new y translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslateY(value) {
            this.array[5] = value;
            return this;
        }
        /**
         * Sets the x and y translations of this transformation.
         * @param x The new x translation.
         * @param y The new y translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslate(x, y) {
            this.array[2] = x;
            this.array[5] = y;
            return this;
        }
        /**
         * Inverts this transformation.
         * @returns This transformation, after it has been inverted.
         */
        invert() {
            const array = this.array;
            const e_00 = array[0];
            const e_01 = array[1];
            const e_02 = array[2];
            const e_10 = array[3];
            const e_11 = array[4];
            const e_12 = array[5];
            const i_00 = e_11;
            const i_01 = -e_10;
            const i_10 = -e_01;
            const i_11 = e_00;
            const i_20 = e_01 * e_12 - e_02 * e_11;
            const i_21 = -(e_00 * e_12 - e_02 * e_10);
            const det = e_00 * i_00 + e_01 * i_01;
            return this.set(i_00 / det, i_10 / det, i_20 / det, i_01 / det, i_11 / det, i_21 / det);
        }
        /**
         * Copies this transformation.
         * @returns A copy of this transformation.
         */
        copy() {
            return new Transform2D().set(this);
        }
        /**
         * Applies this transformation to a 2D vector.
         * @param vec A 2D vector.
         * @param out The vector to which to write the result.
         * @returns The result of applying this transformation to `vec`.
         */
        apply(vec, out) {
            const array = this.array;
            const x = vec[0] * array[0] + vec[1] * array[1] + array[2];
            const y = vec[0] * array[3] + vec[1] * array[4] + array[5];
            return Vec2Math.set(x, y, out);
        }
        /**
         * Changes this transformation to the one that is the result of offsetting this transformation's origin.
         * @param x The x-coordinate of the offset origin.
         * @param y The y-coordinate of the offset origin.
         * @returns This transformation, after it has been changed.
         */
        offsetOrigin(x, y) {
            Transform2D.offsetOriginCache[0].toTranslation(-x, -y);
            Transform2D.offsetOriginCache[1] = this;
            Transform2D.offsetOriginCache[2].toTranslation(x, y);
            return Transform2D.concat(this, Transform2D.offsetOriginCache);
        }
        /**
         * Sets this transformation to the identity transformation.
         * @returns This transformation, after it has been changed.
         */
        toIdentity() {
            return this.set(1, 0, 0, 0, 1, 0);
        }
        /**
         * Sets this transformation to a translation.
         * @param x The x translation.
         * @param y The y translation.
         * @returns This transformation, after it has been changed.
         */
        toTranslation(x, y) {
            return this.set(1, 0, x, 0, 1, y);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toScale(x, y, originX, originY) {
            this.set(x, 0, 0, 0, y, 0);
            if (originX !== undefined && originY !== undefined) {
                this.offsetOrigin(originX, originY);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toRotation(theta, originX, originY) {
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);
            this.set(cos, -sin, 0, sin, cos, 0);
            if (originX !== undefined && originY !== undefined) {
                this.offsetOrigin(originX, originY);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toReflection(theta, originX, originY) {
            const sin = Math.sin(2 * theta);
            const cos = Math.cos(2 * theta);
            this.set(cos, sin, 0, sin, -cos, 0);
            if (originX !== undefined && originY !== undefined) {
                this.offsetOrigin(originX, originY);
            }
            return this;
        }
        /**
         * Adds a translation to this transformation.
         * @param x The x translation.
         * @param y The y translation.
         * @param order The order in which to add the translation, relative to this existing transformation, either
         * `'before'` or `'after'`. Defaults to `'after'`.
         * @returns This transformation, after it has been changed.
         */
        addTranslation(x, y, order = 'after') {
            if (order === 'before') {
                Transform2D.addCache[0].toTranslation(x, y);
                Transform2D.addCache[1].set(this);
            }
            else {
                Transform2D.addCache[0].set(this);
                Transform2D.addCache[1].toTranslation(x, y);
            }
            return Transform2D.concat(this, Transform2D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addScale(x, y, arg3, arg4, arg5) {
            let originX, originY, order;
            if (typeof arg3 === 'number') {
                originX = arg3;
                originY = arg4;
                order = arg5;
            }
            else {
                order = arg3;
            }
            if (order === 'before') {
                originX === undefined ? Transform2D.addCache[0].toScale(x, y) : Transform2D.addCache[0].toScale(x, y, originX, originY);
                Transform2D.addCache[1].set(this);
            }
            else {
                Transform2D.addCache[0].set(this);
                originX === undefined ? Transform2D.addCache[1].toScale(x, y) : Transform2D.addCache[1].toScale(x, y, originX, originY);
            }
            return Transform2D.concat(this, Transform2D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRotation(theta, arg2, arg3, arg4) {
            let originX, originY, order;
            if (typeof arg2 === 'number') {
                originX = arg2;
                originY = arg3;
                order = arg4;
            }
            else {
                order = arg2;
            }
            if (order === 'before') {
                originX === undefined ? Transform2D.addCache[0].toRotation(theta) : Transform2D.addCache[0].toRotation(theta, originX, originY);
                Transform2D.addCache[1].set(this);
            }
            else {
                Transform2D.addCache[0].set(this);
                originX === undefined ? Transform2D.addCache[1].toRotation(theta) : Transform2D.addCache[1].toRotation(theta, originX, originY);
            }
            return Transform2D.concat(this, Transform2D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addReflection(theta, arg2, arg3, arg4) {
            let originX, originY, order;
            if (typeof arg2 === 'number') {
                originX = arg2;
                originY = arg3;
                order = arg4;
            }
            else {
                order = arg2;
            }
            if (order === 'before') {
                originX === undefined ? Transform2D.addCache[0].toReflection(theta) : Transform2D.addCache[0].toReflection(theta, originX, originY);
                Transform2D.addCache[1].set(this);
            }
            else {
                Transform2D.addCache[0].set(this);
                originX === undefined ? Transform2D.addCache[1].toReflection(theta) : Transform2D.addCache[1].toReflection(theta, originX, originY);
            }
            return Transform2D.concat(this, Transform2D.addCache);
        }
        /**
         * Concatenates one or more transformations and returns the result. Concatenating transformations `[A, B, ...]`
         * results in a transformation that is equivalent to first applying `A`, then applying `B`, etc. Note that this order
         * is the _opposite_ of the one resulting from multiplying the individual transformation _matrices_
         * `M_A * M_B * ...`.
         *
         * If the number of transformations to concatenate equals zero, the identity matrix is returned.
         * @param out The transformation to which to write the result.
         * @param transforms The transformations to concatenate, in order.
         * @returns The result of concatenating all transformations in `transforms`.
         */
        static concat(out, transforms) {
            if (transforms.length === 0) {
                return out.toIdentity();
            }
            if (transforms.length === 1) {
                return out.set(transforms[0]);
            }
            let index = 0;
            let next = transforms[index];
            const oldTransform = Transform2D.concatCache[0];
            const newTransform = Transform2D.concatCache[1].set(next);
            const oldArray = oldTransform.array;
            const newArray = newTransform.array;
            const end = transforms.length;
            while (++index < end) {
                next = transforms[index];
                const nextArray = next.array;
                oldTransform.set(newTransform);
                for (let i = 0; i < 3; i++) {
                    for (let j = 0; j < 2; j++) {
                        newArray[j * 3 + i] = oldArray[i] * nextArray[j * 3] + oldArray[3 + i] * nextArray[j * 3 + 1] + (i === 2 ? 1 : 0) * nextArray[j * 3 + 2];
                    }
                }
            }
            return out.set(newTransform);
        }
    }
    Transform2D.offsetOriginCache = [new Transform2D(), undefined, new Transform2D()];
    Transform2D.addCache = [new Transform2D(), new Transform2D()];
    Transform2D.concatCache = [new Transform2D(), new Transform2D()];

    /**
     * A 3D affine transformation. By default, Transform3D objects are initially created as identity transformations.
     */
    class Transform3D {
        constructor() {
            this.array = new Float64Array([
                1, 0, 0, 0,
                0, 1, 0, 0,
                0, 0, 1, 0
            ]);
        }
        /**
         * Gets the parameters of this transformation as a 12-tuple:
         * `[scaleX, skewX(Y), skewX(Z), translateX, skewY(X), scaleY, skewY(Z), translateY, skewZ(X), skewZ(Y), scaleZ, translateZ]`.
         * @returns The parameters of this transformation.
         */
        getParameters() {
            return this.array;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, skewXY, skewXZ, translateX, skewYX, scaleY, skewYZ, translateY, skewZX, skewZY, scaleZ, translateZ) {
            let scaleX = arg1;
            if (arg1 instanceof Transform3D) {
                [scaleX, skewXY, skewXZ, translateX, skewYX, scaleY, skewYZ, translateY, skewZX, skewZY, scaleZ, translateZ] = arg1.array;
            }
            const array = this.array;
            array[0] = scaleX;
            array[1] = skewXY;
            array[2] = skewXZ;
            array[3] = translateX;
            array[4] = skewYX;
            array[5] = scaleY;
            array[6] = skewYZ;
            array[7] = translateY;
            array[8] = skewZX;
            array[9] = skewZY;
            array[10] = scaleZ;
            array[11] = translateZ;
            return this;
        }
        /**
         * Sets the x scaling factor of this transformation.
         * @param value The new x scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScaleX(value) {
            this.array[0] = value;
            return this;
        }
        /**
         * Sets the y scaling factor of this transformation.
         * @param value The new y scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScaleY(value) {
            this.array[5] = value;
            return this;
        }
        /**
         * Sets the z scaling factor of this transformation.
         * @param value The new z scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScaleZ(value) {
            this.array[10] = value;
            return this;
        }
        /**
         * Sets the x and y scaling factors of this transformation.
         * @param x The new x scaling factor.
         * @param y The new y scaling factor.
         * @param z The new z scaling factor.
         * @returns This transformation, after it has been changed.
         */
        setScale(x, y, z) {
            this.array[0] = x;
            this.array[5] = y;
            this.array[10] = z;
            return this;
        }
        /**
         * Sets the x skew factor of this transformation.
         * @param y The new x skew factor along the y axis.
         * @param z The new x skew factor along the z axis.
         * @returns This transformation, after it has been changed.
         */
        setSkewX(y, z) {
            this.array[1] = y;
            this.array[2] = z;
            return this;
        }
        /**
         * Sets the y skew factor of this transformation.
         * @param x The new y skew factor along the x axis.
         * @param z The new y skew factor along the z axis.
         * @returns This transformation, after it has been changed.
         */
        setSkewY(x, z) {
            this.array[4] = x;
            this.array[6] = z;
            return this;
        }
        /**
         * Sets the z skew factor of this transformation.
         * @param x The new z skew factor along the x axis.
         * @param y The new z skew factor along the y axis.
         * @returns This transformation, after it has been changed.
         */
        setSkewZ(x, y) {
            this.array[8] = x;
            this.array[9] = y;
            return this;
        }
        /**
         * Sets the x translation of this transformation.
         * @param value The new x translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslateX(value) {
            this.array[3] = value;
            return this;
        }
        /**
         * Sets the y translation of this transformation.
         * @param value The new y translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslateY(value) {
            this.array[7] = value;
            return this;
        }
        /**
         * Sets the z translation of this transformation.
         * @param value The new z translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslateZ(value) {
            this.array[11] = value;
            return this;
        }
        /**
         * Sets the x and y translations of this transformation.
         * @param x The new x translation.
         * @param y The new y translation.
         * @param z The new z translation.
         * @returns This transformation, after it has been changed.
         */
        setTranslate(x, y, z) {
            this.array[3] = x;
            this.array[7] = y;
            this.array[11] = z;
            return this;
        }
        /**
         * Inverts this transformation.
         * @returns This transformation, after it has been inverted.
         * @throws Error if this transformation cannot be inverted.
         */
        invert() {
            const array = this.array;
            const e_00 = array[0];
            const e_01 = array[1];
            const e_02 = array[2];
            const e_03 = array[3];
            const e_10 = array[4];
            const e_11 = array[5];
            const e_12 = array[6];
            const e_13 = array[7];
            const e_20 = array[8];
            const e_21 = array[9];
            const e_22 = array[10];
            const e_23 = array[11];
            const c_00 = e_11 * e_22 - e_12 * e_21;
            const c_01 = e_12 * e_20 - e_10 * e_22;
            const c_02 = e_10 * e_21 - e_11 * e_20;
            const c_10 = e_02 * e_21 - e_01 * e_22;
            const c_11 = e_00 * e_22 - e_02 * e_20;
            const c_12 = e_01 * e_20 - e_00 * e_21;
            const c_20 = e_01 * e_12 - e_02 * e_11;
            const c_21 = e_02 * e_10 - e_00 * e_12;
            const c_22 = e_00 * e_11 - e_01 * e_10;
            const det = e_00 * c_00 + e_01 * c_01 + e_02 * c_02;
            if (det === 0) {
                throw new Error(`Transform3D: cannot invert transformation with parameters: ${this.array}`);
            }
            const i_00 = c_00 / det;
            const i_01 = c_10 / det;
            const i_02 = c_20 / det;
            const i_10 = c_01 / det;
            const i_11 = c_11 / det;
            const i_12 = c_21 / det;
            const i_20 = c_02 / det;
            const i_21 = c_12 / det;
            const i_22 = c_22 / det;
            const i_03 = -(i_00 * e_03 + i_01 * e_13 + i_02 * e_23);
            const i_13 = -(i_10 * e_03 + i_11 * e_13 + i_12 * e_23);
            const i_23 = -(i_20 * e_03 + i_21 * e_13 + i_22 * e_23);
            return this.set(i_00, i_01, i_02, i_03, i_10, i_11, i_12, i_13, i_20, i_21, i_22, i_23);
        }
        /**
         * Copies this transformation.
         * @returns A copy of this transformation.
         */
        copy() {
            return new Transform3D().set(this);
        }
        /**
         * Applies this transformation to a 3D vector.
         * @param vec A 3D vector.
         * @param out The vector to which to write the result.
         * @returns The result of applying this transformation to `vec`.
         */
        apply(vec, out) {
            const array = this.array;
            const x = vec[0] * array[0] + vec[1] * array[1] + vec[2] * array[2] + array[3];
            const y = vec[0] * array[4] + vec[1] * array[5] + vec[2] * array[6] + array[7];
            const z = vec[0] * array[8] + vec[1] * array[9] + vec[2] * array[10] + array[11];
            return Vec3Math.set(x, y, z, out);
        }
        /**
         * Changes this transformation to the one that is the result of offsetting this transformation's origin.
         * @param x The x-coordinate of the offset origin.
         * @param y The y-coordinate of the offset origin.
         * @param z The z-coordinate of the offset origin.
         * @returns This transformation, after it has been changed.
         */
        offsetOrigin(x, y, z) {
            Transform3D.offsetOriginCache[0].toTranslation(-x, -y, -z);
            Transform3D.offsetOriginCache[1] = this;
            Transform3D.offsetOriginCache[2].toTranslation(x, y, z);
            return Transform3D.concat(this, Transform3D.offsetOriginCache);
        }
        /**
         * Sets this transformation to the identity transformation.
         * @returns This transformation, after it has been changed.
         */
        toIdentity() {
            return this.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0);
        }
        /**
         * Sets this transformation to a translation.
         * @param x The x translation.
         * @param y The y translation.
         * @param z The z translation.
         * @returns This transformation, after it has been changed.
         */
        toTranslation(x, y, z) {
            return this.set(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toScale(x, y, z, originX, originY, originZ) {
            this.set(x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0);
            if (originX !== undefined && originY !== undefined && originZ !== undefined) {
                this.offsetOrigin(originX, originY, originZ);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toRotationX(theta, originX, originY, originZ) {
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);
            this.set(1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0);
            if (originX !== undefined && originY !== undefined && originZ !== undefined) {
                this.offsetOrigin(originX, originY, originZ);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toRotationY(theta, originX, originY, originZ) {
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);
            this.set(cos, 0, sin, 0, 0, 1, 0, 0, -sin, 0, cos, 0);
            if (originX !== undefined && originY !== undefined && originZ !== undefined) {
                this.offsetOrigin(originX, originY, originZ);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toRotationZ(theta, originX, originY, originZ) {
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);
            this.set(cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1, 0);
            if (originX !== undefined && originY !== undefined && originZ !== undefined) {
                this.offsetOrigin(originX, originY, originZ);
            }
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        toRotation(theta, axisX, axisY, axisZ, originX, originY, originZ) {
            const abs = Math.hypot(axisX, axisY, axisZ);
            const ux = axisX / abs;
            const uy = axisY / abs;
            const uz = axisZ / abs;
            const ux_uy = ux * uy;
            const ux_uz = ux * uz;
            const uy_uz = uy * uz;
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);
            const cosCompl = 1 - cos;
            this.set(cos + ux * ux * cosCompl, ux_uy * cosCompl - uz * sin, ux_uz * cosCompl * uy * sin, 0, ux_uy * cosCompl + uz * sin, cos + uy * uy * cosCompl, uy_uz * cosCompl - ux * sin, 0, ux_uz * cosCompl - uy * sin, uy_uz * cosCompl + ux * sin, cos + uz * uz * cosCompl, 0);
            if (originX !== undefined && originY !== undefined && originZ !== undefined) {
                this.offsetOrigin(originX, originY, originZ);
            }
            return this;
        }
        /**
         * Adds a translation to this transformation.
         * @param x The x translation.
         * @param y The y translation.
         * @param z The z translation.
         * @param order The order in which to add the translation, relative to this existing transformation, either
         * `'before'` or `'after'`. Defaults to `'after'`.
         * @returns This transformation, after it has been changed.
         */
        addTranslation(x, y, z, order = 'after') {
            if (order === 'before') {
                Transform3D.addCache[0].toTranslation(x, y, z);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                Transform3D.addCache[1].toTranslation(x, y, z);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addScale(x, y, z, arg4, arg5, arg6, arg7) {
            let originX, originY, originZ, order;
            if (typeof arg4 === 'number') {
                originX = arg4;
                originY = arg5;
                originZ = arg6;
                order = arg7;
            }
            else {
                order = arg4;
            }
            if (order === 'before') {
                originX === undefined ? Transform3D.addCache[0].toScale(x, y, z) : Transform3D.addCache[0].toScale(x, y, z, originX, originY, originZ);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                originX === undefined ? Transform3D.addCache[1].toScale(x, y, z) : Transform3D.addCache[1].toScale(x, y, z, originX, originY, originZ);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRotationX(theta, arg2, arg3, arg4, arg5) {
            let originX, originY, originZ, order;
            if (typeof arg2 === 'number') {
                originX = arg2;
                originY = arg3;
                originZ = arg4;
                order = arg5;
            }
            else {
                order = arg2;
            }
            if (order === 'before') {
                originX === undefined ? Transform3D.addCache[0].toRotationX(theta) : Transform3D.addCache[0].toRotationX(theta, originX, originY, originZ);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                originX === undefined ? Transform3D.addCache[1].toRotationX(theta) : Transform3D.addCache[1].toRotationX(theta, originX, originY, originZ);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRotationY(theta, arg2, arg3, arg4, arg5) {
            let originX, originY, originZ, order;
            if (typeof arg2 === 'number') {
                originX = arg2;
                originY = arg3;
                originZ = arg4;
                order = arg5;
            }
            else {
                order = arg2;
            }
            if (order === 'before') {
                originX === undefined ? Transform3D.addCache[0].toRotationY(theta) : Transform3D.addCache[0].toRotationY(theta, originX, originY, originZ);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                originX === undefined ? Transform3D.addCache[1].toRotationY(theta) : Transform3D.addCache[1].toRotationY(theta, originX, originY, originZ);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRotationZ(theta, arg2, arg3, arg4, arg5) {
            let originX, originY, originZ, order;
            if (typeof arg2 === 'number') {
                originX = arg2;
                originY = arg3;
                originZ = arg4;
                order = arg5;
            }
            else {
                order = arg2;
            }
            if (order === 'before') {
                originX === undefined ? Transform3D.addCache[0].toRotationZ(theta) : Transform3D.addCache[0].toRotationZ(theta, originX, originY, originZ);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                originX === undefined ? Transform3D.addCache[1].toRotationZ(theta) : Transform3D.addCache[1].toRotationZ(theta, originX, originY, originZ);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRotation(theta, axisX, axisY, axisZ, arg5, arg6, arg7, arg8) {
            let originX, originY, originZ, order;
            if (typeof arg5 === 'number') {
                originX = arg5;
                originY = arg6;
                originZ = arg7;
                order = arg8;
            }
            else {
                order = arg5;
            }
            if (order === 'before') {
                originX === undefined
                    ? Transform3D.addCache[0].toRotation(theta, axisX, axisY, axisZ)
                    : Transform3D.addCache[0].toRotation(theta, axisX, axisY, axisZ, originX, originY, originZ);
                Transform3D.addCache[1].set(this);
            }
            else {
                Transform3D.addCache[0].set(this);
                originX === undefined
                    ? Transform3D.addCache[1].toRotation(theta, axisX, axisY, axisZ)
                    : Transform3D.addCache[1].toRotation(theta, axisX, axisY, axisZ, originX, originY, originZ);
            }
            return Transform3D.concat(this, Transform3D.addCache);
        }
        /**
         * Concatenates one or more transformations and returns the result. Concatenating transformations `[A, B, ...]`
         * results in a transformation that is equivalent to first applying `A`, then applying `B`, etc. Note that this order
         * is the _opposite_ of the one resulting from multiplying the individual transformation _matrices_
         * `M_A * M_B * ...`.
         *
         * If the number of transformations to concatenate equals zero, the identity matrix is returned.
         * @param out The transformation to which to write the result.
         * @param transforms The transformations to concatenate, in order.
         * @returns The result of concatenating all transformations in `transforms`.
         */
        static concat(out, transforms) {
            if (transforms.length === 0) {
                return out.toIdentity();
            }
            if (transforms.length === 1) {
                return out.set(transforms[0]);
            }
            let index = 0;
            let next = transforms[index];
            const oldTransform = Transform3D.concatCache[0];
            const newTransform = Transform3D.concatCache[1].set(next);
            const oldArray = oldTransform.array;
            const newArray = newTransform.array;
            const end = transforms.length;
            while (++index < end) {
                next = transforms[index];
                const nextArray = next.array;
                oldTransform.set(newTransform);
                for (let i = 0; i < 4; i++) {
                    for (let j = 0; j < 3; j++) {
                        newArray[j * 4 + i] =
                            oldArray[i] * nextArray[j * 4]
                                + oldArray[4 + i] * nextArray[j * 4 + 1]
                                + oldArray[8 + i] * nextArray[j * 4 + 2]
                                + (i === 3 ? 1 : 0) * nextArray[j * 4 + 3];
                    }
                }
            }
            return out.set(newTransform);
        }
    }
    Transform3D.offsetOriginCache = [new Transform3D(), undefined, new Transform3D()];
    Transform3D.addCache = [new Transform3D(), new Transform3D()];
    Transform3D.concatCache = [new Transform3D(), new Transform3D()];

    /**
     * A perspective transformation.
     */
    class TransformPerspective {
        constructor() {
            this.cameraPos = Vec3Math.create();
            this.surfacePos = Vec3Math.create(0, 0, 1);
            this.cameraPosTransform = new Transform3D();
            this.cameraRotationTransform = new Transform3D();
            this.cameraRotationInverseTransform = new Transform3D();
            this.allCameraTransforms = [this.cameraPosTransform, this.cameraRotationInverseTransform];
            this.fullTransform = new Transform3D();
        }
        /**
         * Gets the position of this transformation's camera, as `[x, y, z]` in world coordinates.
         * @returns The position of this transformation's camera, as `[x, y, z]` in world coordinates.
         */
        getCameraPosition() {
            return this.cameraPos;
        }
        /**
         * Gets the transformation representing the rotation of this transformation's camera.
         * @returns The transformation representing the rotation of this transformation's camera.
         */
        getCameraRotation() {
            return this.cameraRotationTransform;
        }
        /**
         * Gets the position of this transformation's projection surface relative to the camera, as `[x, y, z]` in camera
         * coordinates.
         * @returns The position of this transformation's projection surface relative to the camera, as `[x, y, z]` in camera
         * coordinates.
         */
        getSurfacePosition() {
            return this.cameraPos;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2, arg3) {
            if (arg1 instanceof Float64Array) {
                this._setCameraPosition(arg1);
                this._setCameraRotation(arg2);
                this.setSurfacePosition(arg3);
                Transform3D.concat(this.fullTransform, this.allCameraTransforms);
                return this;
            }
            else {
                return this.set(arg1.getCameraPosition(), arg1.getCameraRotation(), arg1.getSurfacePosition());
            }
        }
        /**
         * Sets the position of this projection's camera. Does not update the full camera transformation.
         * @param cameraPos The position of the camera, as `[x, y, z]` in world coordinates.
         */
        _setCameraPosition(cameraPos) {
            Vec3Math.copy(cameraPos, this.cameraPos);
            this.cameraPosTransform.toTranslation(-cameraPos[0], -cameraPos[1], -cameraPos[2]);
        }
        /**
         * Sets the rotation of this projection's camera. Does not update the full camera transformation.
         * @param cameraRotation A transformation representing the rotation of the camera.
         */
        _setCameraRotation(cameraRotation) {
            this.cameraRotationTransform.set(cameraRotation);
            this.cameraRotationInverseTransform.set(cameraRotation).invert();
        }
        /**
         * Sets the position of this projection's camera.
         * @param cameraPos The position of the camera, as `[x, y, z]` in world coordinates.
         * @returns This transformation, after it has been changed.
         */
        setCameraPosition(cameraPos) {
            this._setCameraPosition(cameraPos);
            Transform3D.concat(this.fullTransform, this.allCameraTransforms);
            return this;
        }
        /**
         * Sets the rotation of this projection's camera.
         * @param cameraRotation A transformation representing the rotation of the camera.
         * @returns This transformation, after it has been changed.
         */
        setCameraRotation(cameraRotation) {
            this._setCameraRotation(cameraRotation);
            Transform3D.concat(this.fullTransform, this.allCameraTransforms);
            return this;
        }
        /**
         * Sets the position of this transformation's projection surface relative to the camera.
         * @param surfacePos The position of the projection surface relative to the camera, as `[x, y, z]` in camera
         * coordinates.
         * @returns This transformation, after it has been changed.
         */
        setSurfacePosition(surfacePos) {
            Vec3Math.copy(surfacePos, this.surfacePos);
            return this;
        }
        /**
         * Copies this transformation.
         * @returns A copy of this transformation.
         */
        copy() {
            return new TransformPerspective().set(this);
        }
        /**
         * Applies this transformation to a 3D vector.
         * @param vec A 3D vector, in world coordinates.
         * @param out The 2D vector to which to write the result.
         * @returns The result of applying this transformation to `vec`.
         */
        apply(vec, out) {
            const transformedVec = this.fullTransform.apply(vec, TransformPerspective.vec3Cache[0]);
            if (Vec3Math.abs(transformedVec) < 1e-7) {
                return Vec2Math.set(0, 0, out);
            }
            if (transformedVec[2] < 0) {
                // vector is behind the camera.
                return Vec2Math.set(NaN, NaN, out);
            }
            const ratio = this.surfacePos[2] / transformedVec[2];
            return Vec2Math.set(transformedVec[0] * ratio + this.surfacePos[0], transformedVec[1] * ratio + this.surfacePos[1], out);
        }
    }
    TransformPerspective.vec3Cache = [Vec3Math.create()];

    /**
     * A Subject which allows a 2D vector to be observed.
     */
    class Vec2Subject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         */
        constructor(value) {
            super();
            this.value = value;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a Vec2Subject.
         * @param initialVal The initial value.
         * @returns A Vec2Subject.
         */
        static create(initialVal) {
            return new Vec2Subject(initialVal);
        }
        /**
         * Creates a Vec2Subject.
         * @param initialVal The initial value.
         * @returns A Vec2Subject.
         * @deprecated Use `Vec2Subject.create()` instead.
         */
        static createFromVector(initialVal) {
            return new Vec2Subject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2) {
            let x, y;
            if (typeof arg1 === 'number') {
                x = arg1;
                y = arg2;
            }
            else {
                x = arg1[0];
                y = arg1[1];
            }
            const equals = SubscribableUtils.NUMERIC_NAN_EQUALITY(x, this.value[0]) && SubscribableUtils.NUMERIC_NAN_EQUALITY(y, this.value[1]);
            if (!equals) {
                Vec2Math.set(x, y, this.value);
                this.notify();
            }
        }
    }
    /**
     * A Subject which allows a 3D vector to be observed.
     */
    class Vec3Subject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         */
        constructor(value) {
            super();
            this.value = value;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a Vec3Subject.
         * @param initialVal The initial value.
         * @returns A Vec3Subject.
         */
        static create(initialVal) {
            return new Vec3Subject(initialVal);
        }
        /**
         * Creates a Vec3Subject.
         * @param initialVal The initial value.
         * @returns A Vec3Subject.
         * @deprecated Use `Vec3Subject.create()` instead.
         */
        static createFromVector(initialVal) {
            return new Vec3Subject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2, arg3) {
            let x, y, z;
            if (typeof arg1 === 'number') {
                x = arg1;
                y = arg2;
                z = arg3;
            }
            else {
                x = arg1[0];
                y = arg1[1];
                z = arg1[2];
            }
            const equals = SubscribableUtils.NUMERIC_NAN_EQUALITY(x, this.value[0])
                && SubscribableUtils.NUMERIC_NAN_EQUALITY(y, this.value[1])
                && SubscribableUtils.NUMERIC_NAN_EQUALITY(z, this.value[2]);
            if (!equals) {
                Vec3Math.set(x, y, z, this.value);
                this.notify();
            }
        }
    }
    /**
     * A Subject which allows a N-D vector to be observed.
     */
    class VecNSubject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         */
        constructor(value) {
            super();
            this.value = value;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a VecNSubject.
         * @param initialVal The initial value.
         * @returns A VecNSubject.
         */
        static create(initialVal) {
            return new VecNSubject(initialVal);
        }
        /**
         * Creates a VecNSubject.
         * @param initialVal The initial value.
         * @returns A VecNSubject.
         * @deprecated Use `VecNSubject.create()` instead.
         */
        static createFromVector(initialVal) {
            return new VecNSubject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, ...args) {
            let array;
            if (typeof arg1 === 'number') {
                array = args;
                args.unshift(arg1);
            }
            else {
                array = arg1;
            }
            if (array.length > this.value.length) {
                throw new RangeError(`VecNSubject: Cannot set ${array.length} components on a vector of length ${this.value.length}`);
            }
            let equals = true;
            const len = array.length;
            for (let i = 0; i < len; i++) {
                if (!SubscribableUtils.NUMERIC_NAN_EQUALITY(array[i], this.value[i])) {
                    equals = false;
                    break;
                }
            }
            if (!equals) {
                this.value.set(array);
                this.notify();
            }
        }
    }

    /**
     * Utility functions for working with arrays.
     */
    class ArrayUtils {
        /**
         * Creates a new array with initialized values.
         * @param length The length of the new array.
         * @param init A function which generates initial values for the new array at each index.
         * @returns A new array of the specified length with initialized values.
         */
        static create(length, init) {
            const newArray = [];
            for (let i = 0; i < length; i++) {
                newArray[i] = init(i);
            }
            return newArray;
        }
        /**
         * Creates a new array containing a sequence of evenly-spaced numbers.
         * @param length The length of the new array.
         * @param start The number contained at index 0 of the new array. Defaults to `0`.
         * @param increment The increment between each successive number in the new array. Defaults to `1`.
         * @returns A new array containing the specified sequence of evenly-spaced numbers.
         */
        static range(length, start = 0, increment = 1) {
            return ArrayUtils.fillRange([], length, 0, start, increment);
        }
        /**
         * Fills an existing array with a sequence of evenly-spaced numbers. The sequence is written to the array in a single
         * contiguous block of consecutive indexes.
         * @param array The array to fill.
         * @param length The length of the number sequence.
         * @param startIndex The index at which to start filling the array. Defaults to `0`.
         * @param start The first number in the sequence. Defaults to {@linkcode startIndex}.
         * @param increment The increment between each successive number in the new array. Defaults to `1`.
         * @returns The array, after it has been filled with the specified number sequence.
         */
        static fillRange(array, length, startIndex = 0, start = startIndex, increment = 1) {
            for (let i = 0; i < length; i++) {
                array[startIndex + i] = start + i * increment;
            }
            return array;
        }
        /**
         * Gets the element at a specific index in an array.
         * @param array An array.
         * @param index The index to access. Negative indexes are supported and access elements starting from the end of the
         * array (`-1` accesses the last element, `-2` the second to last element, etc).
         * @returns The element at the specified index in the array.
         * @throws RangeError if the index is out of bounds.
         */
        static at(array, index) {
            if (index < 0) {
                index += array.length;
            }
            if (index < 0 || index >= array.length) {
                throw new RangeError();
            }
            return array[index];
        }
        /**
         * Gets the element at a specific index in an array, or `undefined` if the index is out of bounds.
         * @param array An array.
         * @param index The index to access. Negative indexes are supported and access elements starting from the end of the
         * array (`-1` accesses the last element, `-2` the second to last element, etc).
         * @returns The element at the specified index in the array, or `undefined` if the index is out of bounds.
         */
        static peekAt(array, index) {
            if (index < 0) {
                index += array.length;
            }
            return array[index];
        }
        /**
         * Gets the first element of an array.
         * @param array An array.
         * @returns The first element of the specified array.
         * @throws RangeError if the array is empty.
         */
        static first(array) {
            if (array.length === 0) {
                throw new RangeError();
            }
            return array[0];
        }
        /**
         * Gets the first element of an array if it is not empty, or `undefined` otherwise.
         * @param array An array.
         * @returns The first element of an array if it is not empty, or `undefined` otherwise.
         */
        static peekFirst(array) {
            return array[0];
        }
        /**
         * Gets the last element of an array.
         * @param array An array.
         * @returns The last element of the specified array.
         * @throws RangeError if the array is empty.
         */
        static last(array) {
            if (array.length === 0) {
                throw new RangeError();
            }
            return array[array.length - 1];
        }
        /**
         * Gets the last element of an array if it is not empty, or `undefined` otherwise.
         * @param array An array.
         * @returns The last element of an array if it is not empty, or `undefined` otherwise.
         */
        static peekLast(array) {
            return array[array.length - 1];
        }
        /**
         * Checks if a certain element is included in an array.
         * @param array An array.
         * @param searchElement The element to search for.
         * @param fromIndex The position in this array at which to begin searching for `searchElement`.
         * @returns Whether the search element is included in the specified array.
         */
        static includes(array, searchElement, fromIndex) {
            return array.includes(searchElement, fromIndex);
        }
        /**
         * Checks if two arrays are equal to each other. This method considers two arrays `a` and `b` if their lengths are
         * equal and `a[i]` equals `b[i]` for every valid index `i`. All empty arrays are considered equal to one another.
         * @param a The first array.
         * @param b The second array.
         * @param equalsFunc The function to use to determine whether two array elements are equal to each other. Defaults
         * to a function which uses the strict equality operator (`===`).
         * @returns Whether the two specified arrays are equal.
         */
        static equals(a, b, equalsFunc = ArrayUtils.STRICT_EQUALS) {
            if (a.length !== b.length) {
                return false;
            }
            for (let i = 0; i < a.length; i++) {
                if (!equalsFunc(a[i], b[i])) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Creates a new array by mapping each element of an existing array using a mapping function, then flattening the
         * mapped elements to a maximum depth of one, leaving the original array intact.
         * @param array An array.
         * @param map A function which is called once on each element of the original array to map it to an arbitrary value.
         * @returns A new array which was created by mapping each element of the specified array, then flattening the mapped
         * elements to a maximum depth of one.
         */
        static flatMap(array, map) {
            const out = [];
            for (let i = 0; i < array.length; i++) {
                const mapped = map(array[i], i, array);
                if (Array.isArray(mapped)) {
                    for (let j = 0; j < mapped.length; j++) {
                        out[out.length] = mapped[j];
                    }
                }
                else {
                    out[out.length] = mapped;
                }
            }
            return out;
        }
        /**
         * Creates a new array by flattening an existing array to a maximum depth, leaving the original array intact. The
         * process of flattening replaces each element in the array that is itself an array with the sequence of elements
         * found in the sub-array, recursively up to the maximum depth.
         * @param array An array.
         * @param depth The maximum depth to which to flatten. Values less than or equal to zero will result in no flattening
         * (in other words, a shallow copy of the original array will be returned). Defaults to `1`.
         * @returns A new array which was created by flattening the specified array to the specified maximum depth.
         */
        static flat(array, depth = 1) {
            const out = [];
            this.flatHelper(array, depth, 0, out);
            return out;
        }
        /**
         * Recursively flattens an array and writes the flattened sequence of elements into another array.
         * @param array The array to flatten.
         * @param maxDepth The maximum depth to which to flatten.
         * @param depth The current flattening depth.
         * @param out The array to which to write the flattened sequence of elements.
         */
        static flatHelper(array, maxDepth, depth, out) {
            for (let i = 0; i < array.length; i++) {
                const element = array[i];
                if (Array.isArray(element) && depth < maxDepth) {
                    this.flatHelper(element, maxDepth, depth + 1, out);
                }
                else {
                    out[out.length] = element;
                }
            }
        }
        /**
         * Performs a shallow copy of an array. After the operation is complete, the target array will have the same
         * length and the same elements in the same order as the source array.
         * @param source The array to copy.
         * @param target The array to copy into. If not defined, a new array will be created.
         * @returns The target array, after the source array has been copied into it.
         */
        static shallowCopy(source, target = []) {
            target.length = source.length;
            for (let i = 0; i < source.length; i++) {
                target[i] = source[i];
            }
            return target;
        }
        /**
         * Performs a binary search on a sorted array to find the index of the first or last element in the array whose
         * sorting order is equal to a query element. If no such element in the array exists, `-(index + 1)` is returned,
         * where `index` is the index at which the query element would be found if it were contained in the sorted array.
         * @param array An array.
         * @param element The element to search for.
         * @param comparator A function which determines the sorting order of elements in the array. The function should
         * return a negative number if the first element is to be sorted before the second, a positive number if the first
         * element is to be sorted after the second, or zero if both elements are to be sorted equivalently.
         * @param first If `true`, this method will find the first (lowest) matching index if there are multiple matching
         * indexes, otherwise this method will find the last (highest) matching index. Defaults to `true`.
         * @returns The index of the first (if `first` is `true`) or last (if `first` is `false`) element in the specified
         * array whose sorting order is equal to the query element, or `-(index + 1)`, where `index` is the index at which
         * the query element would be found if it were contained in the sorted array, if no element in the array has a
         * sorting order equal to the query.
         */
        static binarySearch(array, element, comparator, first = true) {
            let min = 0;
            let max = array.length;
            let index = Math.floor((min + max) / 2);
            while (min < max) {
                const compare = comparator(element, array[index]);
                if (compare < 0) {
                    max = index;
                }
                else if (compare > 0) {
                    min = index + 1;
                }
                else {
                    const delta = first ? -1 : 1;
                    while (index + delta >= 0 && index + delta < array.length && comparator(element, array[index + delta]) === 0) {
                        index += delta;
                    }
                    return index;
                }
                index = Math.floor((min + max) / 2);
            }
            return -(index + 1);
        }
        /**
         * Gets the length of the longest string in the array.
         * @param array The array to search in.
         * @returns length of the longest string
         */
        static getMaxStringLength(array) {
            return array.reduce((accum, curr) => curr.length > accum ? curr.length : accum, 0);
        }
    }
    ArrayUtils.STRICT_EQUALS = (a, b) => a === b;

    /**
     * A utility class for working with geographic math using a spherical datum.
     */
    class GeoMath {
    }
    /**
     * The standard angular tolerance, defined as the maximum allowed angular distance (also equal to the great-circle
     * distance) between two equal points, in radians.
     */
    GeoMath.ANGULAR_TOLERANCE = 1e-7; // ~61cm

    /**
     * A read-only wrapper for a GeoPoint.
     */
    class GeoPointReadOnly {
        /**
         * Constructor.
         * @param source - the source of the new read-only point.
         */
        constructor(source) {
            this.source = source;
        }
        /**
         * The latitude of this point, in degrees.
         * @returns the latitude of this point.
         */
        get lat() {
            return this.source.lat;
        }
        /**
         * The longitude of this point, in degrees.
         * @returns the longitude of this point.
         */
        get lon() {
            return this.source.lon;
        }
        /** @inheritDoc */
        isValid() {
            return this.source.isValid();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        distance(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.distance(arg1, arg2);
            }
            else {
                return this.source.distance(arg1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        distanceRhumb(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.distanceRhumb(arg1, arg2);
            }
            else {
                return this.source.distanceRhumb(arg1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingTo(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.bearingTo(arg1, arg2);
            }
            else {
                return this.source.bearingTo(arg1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingFrom(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.bearingFrom(arg1, arg2);
            }
            else {
                return this.source.bearingFrom(arg1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingRhumb(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return this.source.bearingRhumb(arg1, arg2);
            }
            else {
                return this.source.bearingRhumb(arg1);
            }
        }
        /**
         * Offsets this point by an initial bearing and distance along a great circle.
         * @param bearing The bearing along which to offset, in degrees relative to true north. If this point is one of the
         * poles, then the bearing will be measured relative to the direction in which the meridian defined by this point's
         * longitude crosses the pole rather than the direction of true north.
         * @param distance The distance, in great-arc radians, by which to offset.
         * @param out The GeoPoint to which to write the result. If not supplied, a new GeoPoint object is created.
         * @returns The offset point.
         * @throws Error if argument `out` is undefined.
         */
        offset(bearing, distance, out) {
            if (!out) {
                throw new Error('Cannot mutate a read-only GeoPoint.');
            }
            return this.source.offset(bearing, distance, out);
        }
        /**
         * Offsets this point by a constant bearing and distance along a rhumb line.
         * @param bearing The true bearing, in degrees, by which to offset.
         * @param distance The distance, in great-arc radians, by which to offset.
         * @param out The GeoPoint to which to write the result. If not supplied, a new GeoPoint object is created.
         * @returns The offset point.
         * @throws Error if argument `out` is undefined.
         */
        offsetRhumb(bearing, distance, out) {
            if (!out) {
                throw new Error('Cannot mutate a read-only GeoPoint.');
            }
            return this.source.offsetRhumb(bearing, distance, out);
        }
        /**
         * Gets the antipode of this point.
         * @param out The GeoPoint ot which to write the result.
         * @returns The antipode of this point.
         * @throws Error if argument `out` is undefined.
         */
        antipode(out) {
            if (!out) {
                throw new Error('Cannot mutate a read-only GeoPoint.');
            }
            return this.source.antipode(out);
        }
        /**
         * Calculates the cartesian (x, y, z) representation of this point, in units of great-arc radians. By convention,
         * in the cartesian coordinate system the origin is at the center of the Earth, the positive x-axis passes through
         * 0 degrees N, 0 degrees E, and the positive z-axis passes through the north pole.
         * @param out The vector array to which to write the result.
         * @returns The cartesian representation of this point.
         */
        toCartesian(out) {
            return this.source.toCartesian(out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        equals(arg1, arg2, arg3) {
            if (typeof arg1 === 'number') {
                return this.source.equals(arg1, arg2, arg3);
            }
            else {
                return this.source.equals(arg1, arg2);
            }
        }
        /** @inheritdoc */
        copy(to) {
            return this.source.copy(to);
        }
    }
    /**
     * A point on Earth's surface. This class uses a spherical Earth model.
     */
    class GeoPoint {
        /**
         * Constructor.
         * @param lat The latitude, in degrees.
         * @param lon The longitude, in degrees.
         */
        constructor(lat, lon) {
            this._lat = 0;
            this._lon = 0;
            this.set(lat, lon);
            this.readonly = new GeoPointReadOnly(this);
        }
        /**
         * The latitude of this point, in degrees.
         * @returns the latitude of this point.
         */
        get lat() {
            return this._lat;
        }
        /**
         * The longitude of this point, in degrees.
         * @returns the longitude of this point.
         */
        get lon() {
            return this._lon;
        }
        /**
         * Converts an argument list consisting of either a LatLonInterface or lat/lon coordinates into an equivalent
         * LatLonInterface.
         * @param arg1 Argument 1.
         * @param arg2 Argument 2.
         * @returns A LatLonInterface.
         */
        static asLatLonInterface(arg1, arg2) {
            if (typeof arg1 === 'number') {
                return GeoPoint.tempGeoPoint.set(arg1, arg2);
            }
            else {
                return arg1;
            }
        }
        /**
         * Converts an argument list consisting of either a 3D vector or x, y, z components into an equivalent 3D vector.
         * @param arg1 Argument 1.
         * @param arg2 Argument 2.
         * @param arg3 Argument 3.
         * @returns A 3D vector.
         */
        static asVec3(arg1, arg2, arg3) {
            if (typeof arg1 === 'number') {
                return Vec3Math.set(arg1, arg2, arg3, GeoPoint.tempVec3);
            }
            else {
                return arg1;
            }
        }
        /** @inheritDoc */
        isValid() {
            return isFinite(this._lat) && isFinite(this._lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2) {
            let lat, lon;
            if (typeof arg1 === 'number') {
                lat = arg1;
                lon = arg2;
            }
            else {
                lat = arg1.lat;
                lon = arg1.lon;
            }
            lat = MathUtils.normalizeAngleDeg(lat, -180);
            lon = MathUtils.normalizeAngleDeg(lon, -180);
            if (Math.abs(lat) > 90) {
                lat = 180 - lat;
                lat = MathUtils.normalizeAngleDeg(lat, -180);
                lon += 180;
                lon = MathUtils.normalizeAngleDeg(lon, -180);
            }
            this._lat = lat;
            this._lon = lon;
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setFromCartesian(arg1, arg2, arg3) {
            const vec = GeoPoint.asVec3(arg1, arg2, arg3);
            const theta = Vec3Math.theta(vec);
            const phi = Vec3Math.phi(vec);
            return this.set(90 - theta * Avionics.Utils.RAD2DEG, phi * Avionics.Utils.RAD2DEG);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        distance(arg1, arg2) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            return GeoPoint.distance(this.lat, this.lon, other.lat, other.lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        distanceRhumb(arg1, arg2) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            return GeoPoint.distanceRhumb(this.lat, this.lon, other.lat, other.lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingTo(arg1, arg2) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            return GeoPoint.initialBearing(this.lat, this.lon, other.lat, other.lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingFrom(arg1, arg2) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            return GeoPoint.finalBearing(other.lat, other.lon, this.lat, this.lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        bearingRhumb(arg1, arg2) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            return GeoPoint.bearingRhumb(this.lat, this.lon, other.lat, other.lon);
        }
        /**
         * Offsets this point by an initial bearing and distance along a great circle.
         * @param bearing The bearing along which to offset, in degrees relative to true north. If this point is one of the
         * poles, then the bearing will be measured relative to the direction in which the meridian defined by this point's
         * longitude crosses the pole rather than the direction of true north.
         * @param distance The distance, in great-arc radians, by which to offset.
         * @param out The GeoPoint to which to write the result. By default this point.
         * @returns The offset point.
         */
        offset(bearing, distance, out) {
            let offsetLat;
            let offsetLon;
            if (Math.abs(this.lat) === 90) {
                // This point is one of the poles. Bearing (in the traditional sense) is undefined at the poles. Therefore, we
                // will adopt a special definition here where bearing is defined clockwise (looking from above) from the
                // direction that the meridian defined by this point's longitude crosses the pole. This will make the result of
                // the offset consistent with the bearingTo() and bearingFrom() methods. In other words, the bearing from the
                // initial point (the pole) to a point offset by bearing X as determined by bearingTo() will always be equal to
                // X (assuming the offset distance is less than pi).
                // These values will be normalized by the call to .set() at the end.
                offsetLat = this.lat + distance * Avionics.Utils.RAD2DEG;
                offsetLon = this.lon + bearing * (this.lat > 0 ? -1 : 1);
            }
            else {
                const latRad = this.lat * Avionics.Utils.DEG2RAD;
                const lonRad = this.lon * Avionics.Utils.DEG2RAD;
                const sinLat = Math.sin(latRad);
                const cosLat = Math.cos(latRad);
                const sinBearing = Math.sin(bearing * Avionics.Utils.DEG2RAD);
                const cosBearing = Math.cos(bearing * Avionics.Utils.DEG2RAD);
                const angularDistance = distance;
                const sinAngularDistance = Math.sin(angularDistance);
                const cosAngularDistance = Math.cos(angularDistance);
                const offsetLatRad = Math.asin(sinLat * cosAngularDistance + cosLat * sinAngularDistance * cosBearing);
                const offsetLonDeltaRad = Math.atan2(sinBearing * sinAngularDistance * cosLat, cosAngularDistance - sinLat * Math.sin(offsetLatRad));
                offsetLat = offsetLatRad * Avionics.Utils.RAD2DEG;
                offsetLon = (lonRad + offsetLonDeltaRad) * Avionics.Utils.RAD2DEG;
            }
            return (out !== null && out !== void 0 ? out : this).set(offsetLat, offsetLon);
        }
        /**
         * Offsets this point by a constant bearing and distance along a rhumb line.
         * @param bearing The true bearing, in degrees, by which to offset.
         * @param distance The distance, in great-arc radians, by which to offset.
         * @param out The GeoPoint to which to write the result. By default this point.
         * @returns The offset point.
         */
        offsetRhumb(bearing, distance, out) {
            const latRad = this.lat * Avionics.Utils.DEG2RAD;
            const lonRad = this.lon * Avionics.Utils.DEG2RAD;
            const bearingRad = bearing * Avionics.Utils.DEG2RAD;
            const deltaLat = distance * Math.cos(bearingRad);
            let offsetLat = latRad + deltaLat;
            let offsetLon;
            if (Math.abs(offsetLat) >= Math.PI / 2) {
                // you can't technically go past the poles along a rhumb line, so we will simply terminate the path at the pole
                offsetLat = Math.sign(offsetLat) * 90;
                offsetLon = 0; // since longitude is meaningless at the poles, we'll arbitrarily pick a longitude of 0 degrees.
            }
            else {
                const deltaPsi = GeoPoint.deltaPsi(latRad, offsetLat);
                const correction = GeoPoint.rhumbCorrection(deltaPsi, latRad, offsetLat);
                const deltaLon = distance * Math.sin(bearingRad) / correction;
                offsetLon = lonRad + deltaLon;
                offsetLat *= Avionics.Utils.RAD2DEG;
                offsetLon *= Avionics.Utils.RAD2DEG;
            }
            return (out !== null && out !== void 0 ? out : this).set(offsetLat, offsetLon);
        }
        /**
         * Gets the antipode of this point.
         * @param out The GeoPoint to which to write the results. By default this point.
         * @returns The antipode of this point.
         */
        antipode(out) {
            return (out !== null && out !== void 0 ? out : this).set(-this._lat, this._lon + 180);
        }
        /** @inheritdoc */
        toCartesian(out) {
            return GeoPoint.sphericalToCartesian(this, out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        equals(arg1, arg2, arg3) {
            const other = GeoPoint.asLatLonInterface(arg1, arg2);
            if (other) {
                if (isNaN(this._lat) && isNaN(this._lon) && isNaN(other.lat) && isNaN(other.lon)) {
                    return true;
                }
                const tolerance = typeof arg1 === 'number' ? arg3 : arg2;
                const distance = this.distance(other);
                return !isNaN(distance) && distance <= (tolerance !== null && tolerance !== void 0 ? tolerance : GeoPoint.EQUALITY_TOLERANCE);
            }
            else {
                return false;
            }
        }
        /** @inheritdoc */
        copy(to) {
            return to ? to.set(this.lat, this.lon) : new GeoPoint(this.lat, this.lon);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static sphericalToCartesian(arg1, arg2, arg3) {
            const point = GeoPoint.asLatLonInterface(arg1, arg2);
            const theta = (90 - point.lat) * Avionics.Utils.DEG2RAD;
            const phi = point.lon * Avionics.Utils.DEG2RAD;
            return Vec3Math.setFromSpherical(1, theta, phi, arg3 !== null && arg3 !== void 0 ? arg3 : arg2);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static equals(arg1, arg2, arg3, arg4, arg5) {
            if (arg1 instanceof Float64Array) {
                return GeoPoint.distance(arg1, arg2) <= (arg3 !== null && arg3 !== void 0 ? arg3 : GeoPoint.EQUALITY_TOLERANCE);
            }
            else if (typeof arg1 === 'number') {
                return GeoPoint.distance(arg1, arg2, arg3, arg4) <= (arg5 !== null && arg5 !== void 0 ? arg5 : GeoPoint.EQUALITY_TOLERANCE);
            }
            else {
                return GeoPoint.distance(arg1, arg2) <= (arg3 !== null && arg3 !== void 0 ? arg3 : GeoPoint.EQUALITY_TOLERANCE);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static distance(arg1, arg2, arg3, arg4) {
            if (arg1 instanceof Float64Array) {
                return Vec3Math.unitAngle(arg1, arg2);
            }
            else {
                let lat1, lon1, lat2, lon2;
                if (typeof arg1 === 'number') {
                    lat1 = arg1;
                    lon1 = arg2;
                    lat2 = arg3;
                    lon2 = arg4;
                }
                else {
                    lat1 = arg1.lat;
                    lon1 = arg1.lon;
                    lat2 = arg2.lat;
                    lon2 = arg2.lon;
                }
                lat1 *= Avionics.Utils.DEG2RAD;
                lon1 *= Avionics.Utils.DEG2RAD;
                lat2 *= Avionics.Utils.DEG2RAD;
                lon2 *= Avionics.Utils.DEG2RAD;
                // haversine formula
                const sinHalfDeltaLat = Math.sin((lat2 - lat1) / 2);
                const sinHalfDeltaLon = Math.sin((lon2 - lon1) / 2);
                const a = sinHalfDeltaLat * sinHalfDeltaLat + Math.cos(lat1) * Math.cos(lat2) * sinHalfDeltaLon * sinHalfDeltaLon;
                return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static distanceRhumb(arg1, arg2, arg3, arg4) {
            let lat1, lon1, lat2, lon2;
            if (typeof arg1 === 'number') {
                lat1 = arg1 * Avionics.Utils.DEG2RAD;
                lon1 = arg2 * Avionics.Utils.DEG2RAD;
                lat2 = arg3 * Avionics.Utils.DEG2RAD;
                lon2 = arg4 * Avionics.Utils.DEG2RAD;
            }
            else if (arg1 instanceof Float64Array) {
                const point1 = GeoPoint.tempGeoPoint.setFromCartesian(arg1);
                lat1 = point1.lat;
                lon1 = point1.lon;
                const point2 = GeoPoint.tempGeoPoint.setFromCartesian(arg2);
                lat2 = point2.lat;
                lon2 = point2.lon;
            }
            else {
                lat1 = arg1.lat;
                lon1 = arg1.lon;
                lat2 = arg2.lat;
                lon2 = arg2.lon;
            }
            const deltaLat = lat2 - lat1;
            let deltaLon = lon2 - lon1;
            const deltaPsi = GeoPoint.deltaPsi(lat1, lat2);
            const correction = GeoPoint.rhumbCorrection(deltaPsi, lat1, lat2);
            if (Math.abs(deltaLon) > Math.PI) {
                deltaLon += -Math.sign(deltaLon) * 2 * Math.PI;
            }
            return Math.sqrt(deltaLat * deltaLat + correction * correction * deltaLon * deltaLon);
        }
        /**
         * Calculates the initial true bearing (forward azimuth) from one point to another along the great circle connecting
         * the two.
         * @param lat1 The latitude of the initial point, in degrees.
         * @param lon1 The longitude of the initial point, in degrees.
         * @param lat2 The latitude of the final point, in degrees.
         * @param lon2 The longitude of the final point, in degrees.
         * @returns The initial true bearing, in degrees, from the initial point to the final point along the great circle
         * connecting the two, or `NaN` if the two points are coincident or antipodal. If the initial point is one of the
         * poles, then the bearing will be expressed relative to the direction in which the meridian defined by the initial
         * point's longitude crosses the pole rather than true north.
         */
        static initialBearing(lat1, lon1, lat2, lon2) {
            lat1 *= Avionics.Utils.DEG2RAD;
            lat2 *= Avionics.Utils.DEG2RAD;
            lon1 *= Avionics.Utils.DEG2RAD;
            lon2 *= Avionics.Utils.DEG2RAD;
            const cosLat2 = Math.cos(lat2);
            const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * cosLat2 * Math.cos(lon2 - lon1);
            const y = Math.sin(lon2 - lon1) * cosLat2;
            if (Math.abs(x) < 1e-14 && Math.abs(y) < 1e-14) {
                // The two points are coincident or antipodal. There is no unique great circle that connects them.
                return NaN;
            }
            const bearing = Math.atan2(y, x) * Avionics.Utils.RAD2DEG;
            return (bearing + 360) % 360; // enforce range [0, 360)
        }
        /**
         * Calculates the final true bearing from one point to another along the great circle connecting the two.
         * @param lat1 The latitude of the initial point, in degrees.
         * @param lon1 The longitude of the initial point, in degrees.
         * @param lat2 The latitude of the final point, in degrees.
         * @param lon2 The longitude of the final point, in degrees.
         * @returns The final true bearing, in degrees, from the initial point to the final point along the great circle
         * connecting the two, or `NaN` if the two points are coincident or antipodal. If the final point is one of the
         * poles, then the bearing will be expressed relative to the direction in which the meridian defined by the final
         * point's longitude crosses the pole rather than true north.
         */
        static finalBearing(lat1, lon1, lat2, lon2) {
            return (GeoPoint.initialBearing(lat2, lon2, lat1, lon1) + 180) % 360;
        }
        /**
         * Calculates the constant true bearing from one point to another along the rhumb line connecting the two.
         * @param lat1 The latitude of the initial point, in degrees.
         * @param lon1 The longitude of the initial point, in degrees.
         * @param lat2 The latitude of the final point, in degrees.
         * @param lon2 The longitude of the final point, in degrees.
         * @returns The constant true bearing, in degrees, from the initial point to the final point along the rhumb line
         * connecting the two.
         */
        static bearingRhumb(lat1, lon1, lat2, lon2) {
            lat1 *= Avionics.Utils.DEG2RAD;
            lat2 *= Avionics.Utils.DEG2RAD;
            lon1 *= Avionics.Utils.DEG2RAD;
            lon2 *= Avionics.Utils.DEG2RAD;
            let deltaLon = lon2 - lon1;
            const deltaPsi = GeoPoint.deltaPsi(lat1, lat2);
            if (Math.abs(deltaLon) > Math.PI) {
                deltaLon += -Math.sign(deltaLon) * 2 * Math.PI;
            }
            return Math.atan2(deltaLon, deltaPsi) * Avionics.Utils.RAD2DEG;
        }
        /**
         * Calculates the difference in isometric latitude from a pair of geodetic (geocentric) latitudes.
         * @param latRad1 Geodetic latitude 1, in radians.
         * @param latRad2 Geodetic latitude 2, in radians.
         * @returns The difference in isometric latitude from latitude 1 to latitude 2, in radians.
         */
        static deltaPsi(latRad1, latRad2) {
            return Math.log(Math.tan(latRad2 / 2 + Math.PI / 4) / Math.tan(latRad1 / 2 + Math.PI / 4));
        }
        /**
         * Calculates the rhumb correction factor between two latitudes.
         * @param deltaPsi The difference in isometric latitude beween the two latitudes.
         * @param latRad1 Geodetic latitude 1, in radians.
         * @param latRad2 Geodetic latitude 2, in radians.
         * @returns The rhumb correction factor between the two latitudes.
         */
        static rhumbCorrection(deltaPsi, latRad1, latRad2) {
            return Math.abs(deltaPsi) > 1e-12 ? ((latRad2 - latRad1) / deltaPsi) : Math.cos(latRad1);
        }
    }
    /**
     * The default equality tolerance, defined as the maximum allowed distance between two equal points in great-arc
     * radians.
     */
    GeoPoint.EQUALITY_TOLERANCE = GeoMath.ANGULAR_TOLERANCE;
    GeoPoint.tempVec3 = new Float64Array(3);
    GeoPoint.tempGeoPoint = new GeoPoint(0, 0);

    /**
     * A circle on Earth's surface, defined as the set of points on the Earth's surface equidistant (as measured
     * geodetically) from a central point.
     */
    class GeoCircle {
        /**
         * Creates a new instance of GeoCircle.
         * @param center The center of the new circle, represented as a position vector in the standard geographic
         * cartesian reference system.
         * @param radius The radius of the new circle in great-arc radians.
         */
        constructor(center, radius) {
            this._center = Vec3Math.create();
            this._radius = 0;
            this._sinRadius = 0;
            this.set(center, radius);
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The center of this circle.
         */
        get center() {
            return this._center;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The radius of this circle, in great-arc radians.
         */
        get radius() {
            return this._radius;
        }
        /**
         * Checks whether this circle is a valid. A circle is valid if and only if it has a finite radius and its center is a
         * position vector pointing to a point on the earth's surface.
         * @returns Whether this circle is valid.
         */
        isValid() {
            return Vec3Math.isFinite(this._center) && isFinite(this._radius);
        }
        /**
         * Checks whether this circle is a great circle, or equivalently, whether its radius is equal to pi / 2 great-arc
         * radians.
         * @returns Whether this circle is a great circle.
         */
        isGreatCircle() {
            return this._radius === Math.PI / 2;
        }
        /**
         * Calculates the length of an arc along this circle subtended by a central angle.
         * @param angle A central angle, in radians.
         * @returns The length of the arc subtended by the angle, in great-arc radians.
         */
        arcLength(angle) {
            return this._sinRadius * angle;
        }
        /**
         * Calculates the central angle which subtends an arc along this circle of given length.
         * @param length An arc length, in great-arc radians.
         * @returns The central angle which subtends an arc along this circle of the given length, in radians.
         */
        angularWidth(length) {
            return length / this._sinRadius;
        }
        /**
         * Sets the center and radius of this circle.
         * @param center The new center.
         * @param radius The new radius in great-arc radians.
         * @returns This circle, after it has been changed.
         */
        set(center, radius) {
            if (center instanceof Float64Array) {
                if (Vec3Math.abs(center) === 0) {
                    // If center has no direction, then set the vector to NaN.
                    Vec3Math.set(NaN, NaN, NaN, this._center);
                }
                else {
                    Vec3Math.normalize(center, this._center);
                }
            }
            else {
                GeoPoint.sphericalToCartesian(center, this._center);
            }
            return this._setRadius(radius);
        }
        /**
         * Sets the center and radius of this circle. This method does not validate or normalize the provided center vector.
         * @param center The new center.
         * @param radius The new radius in great-arc radians.
         * @returns This circle, after it has been changed.
         */
        _set(center, radius) {
            Vec3Math.copy(center, this._center);
            this._radius = Math.abs(radius) % Math.PI;
            this._sinRadius = Math.sin(this._radius);
            return this;
        }
        /**
         * Sets the radius of this circle.
         * @param radius The new radius in great-arc radians.
         * @returns This circle, after it has been changed.
         */
        _setRadius(radius) {
            this._radius = Math.abs(radius) % Math.PI;
            this._sinRadius = Math.sin(this._radius);
            return this;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setAsGreatCircle(arg1, arg2) {
            GeoCircle._getGreatCircleNormal(arg1, arg2, this._center);
            return this._setRadius(Math.PI / 2);
        }
        /**
         * Reverses the direction of this circle. This sets the center of the circle to its antipode and the radius to its
         * complement with `Math.PI`.
         * @returns This circle, after it has been reversed.
         */
        reverse() {
            Vec3Math.multScalar(this._center, -1, this._center);
            this._radius = Math.PI - this._radius;
            return this;
        }
        /**
         * Gets the distance from a point to the center of this circle, in great-arc radians.
         * @param point The point to which to measure the distance.
         * @returns the distance from the point to the center of this circle.
         */
        distanceToCenter(point) {
            if (point instanceof Float64Array) {
                point = Vec3Math.normalize(point, GeoCircle.vec3Cache[0]);
            }
            else {
                point = GeoPoint.sphericalToCartesian(point, GeoCircle.vec3Cache[0]);
            }
            return Vec3Math.unitAngle(point, this._center);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        closest(point, out) {
            if (!(point instanceof Float64Array)) {
                point = GeoPoint.sphericalToCartesian(point, GeoCircle.vec3Cache[0]);
            }
            const offset = Vec3Math.multScalar(this._center, Math.cos(this._radius), GeoCircle.vec3Cache[1]);
            const dot = Vec3Math.dot(Vec3Math.sub(point, offset, GeoCircle.vec3Cache[2]), this._center);
            const planeProjected = Vec3Math.sub(point, Vec3Math.multScalar(this._center, dot, GeoCircle.vec3Cache[2]), GeoCircle.vec3Cache[2]);
            if (Vec3Math.dot(planeProjected, planeProjected) === 0 || Math.abs(Vec3Math.dot(planeProjected, this._center)) === 1) {
                // the point is equidistant from all points on this circle
                return out instanceof GeoPoint ? out.set(NaN, NaN) : Vec3Math.set(NaN, NaN, NaN, out);
            }
            const displacement = Vec3Math.multScalar(Vec3Math.normalize(Vec3Math.sub(planeProjected, offset, GeoCircle.vec3Cache[2]), GeoCircle.vec3Cache[2]), this._sinRadius, GeoCircle.vec3Cache[2]);
            const closest = Vec3Math.add(offset, displacement, GeoCircle.vec3Cache[2]);
            return out instanceof Float64Array ? Vec3Math.normalize(closest, out) : out.setFromCartesian(closest);
        }
        /**
         * Calculates and returns the great-circle distance from a specified point to the closest point that lies on this
         * circle. In other words, calculates the shortest distance from a point to this circle. The distance is signed, with
         * positive distances representing deviation away from the center of the circle, and negative distances representing
         * deviation toward the center of the circle.
         * @param point A point, represented as either a position vector or lat/long coordinates.
         * @returns the great circle distance, in great-arc radians, from the point to the closest point on this circle.
         */
        distance(point) {
            const distanceToCenter = this.distanceToCenter(point);
            return distanceToCenter - this._radius;
        }
        /**
         * Checks whether a point lies on this circle.
         * @param point A point, represented as either a position vector or lat/long coordinates.
         * @param tolerance The error tolerance, in great-arc radians, of this operation. Defaults to
         * `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns whether the point lies on this circle.
         */
        includes(point, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const distance = this.distance(point);
            return Math.abs(distance) < tolerance;
        }
        /**
         * Checks whether a point lies within the boundary defined by this circle. This is equivalent to checking whether
         * the distance of the point from the center of this circle is less than or equal to this circle's radius.
         * @param point A point, represented as either a position vector or lat/long coordinates.
         * @param inclusive Whether points that lie on this circle should pass the check. True by default.
         * @param tolerance The error tolerance, in great-arc radians, of this operation. Defaults to
         * `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns whether the point lies within the boundary defined by this circle.
         */
        encircles(point, inclusive = true, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const distance = this.distance(point);
            return inclusive
                ? distance <= tolerance
                : distance < -tolerance;
        }
        /**
         * Gets the angular distance along an arc between two points that lie on this circle. The arc extends from the first
         * point to the second in a counterclockwise direction when viewed from above the center of the circle.
         * @param start A point on this circle which marks the beginning of an arc.
         * @param end A point on this circle which marks the end of an arc.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `start` and `end` lie on this circle.
         * Defaults to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @param equalityTolerance The angular tolerance for considering the start and end points to be equal, in radians.
         * If the absolute (direction-agnostic) angular distance between the start and end points is less than or equal to
         * this value, then the zero will be returned. Defaults to `0`.
         * @returns the angular width of the arc between the two points, in radians.
         * @throws Error if either point does not lie on this circle.
         */
        angleAlong(start, end, tolerance = GeoCircle.ANGULAR_TOLERANCE, equalityTolerance = 0) {
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, GeoCircle.vec3Cache[1]);
            }
            if (!(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, GeoCircle.vec3Cache[2]);
            }
            if (tolerance < Math.PI && !this.includes(start, tolerance) || !this.includes(end, tolerance)) {
                throw new Error(`GeoCircle: at least one of the two specified arc end points does not lie on this circle (start point distance of ${this.distance(start)}, end point distance of ${this.distance(end)}, vs tolerance of ${tolerance}).`);
            }
            if (this._radius <= GeoCircle.ANGULAR_TOLERANCE) {
                return 0;
            }
            const startRadialNormal = Vec3Math.normalize(Vec3Math.cross(this._center, start, GeoCircle.vec3Cache[3]), GeoCircle.vec3Cache[3]);
            const endRadialNormal = Vec3Math.normalize(Vec3Math.cross(this._center, end, GeoCircle.vec3Cache[4]), GeoCircle.vec3Cache[4]);
            const angularDistance = Math.acos(Utils.Clamp(Vec3Math.dot(startRadialNormal, endRadialNormal), -1, 1));
            const isArcGreaterThanSemi = Vec3Math.dot(startRadialNormal, end) < 0;
            const angle = isArcGreaterThanSemi ? MathUtils.TWO_PI - angularDistance : angularDistance;
            return angle >= MathUtils.TWO_PI - equalityTolerance || angle <= equalityTolerance ? 0 : angle;
        }
        /**
         * Gets the distance along an arc between two points that lie on this circle. The arc extends from the first point
         * to the second in a counterclockwise direction when viewed from above the center of the circle.
         * @param start A point on this circle which marks the beginning of an arc.
         * @param end A point on this circle which marks the end of an arc.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `start` and `end` lie on this circle.
         * Defaults to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @param equalityTolerance The tolerance for considering the start and end points to be equal, in great-arc radians.
         * If the absolute (direction-agnostic) along-arc distance between the start and end points is less than or equal to
         * this value, then the zero will be returned. Defaults to `0`.
         * @returns the length of the arc between the two points, in great-arc radians.
         * @throws Error if either point does not lie on this circle.
         */
        distanceAlong(start, end, tolerance = GeoCircle.ANGULAR_TOLERANCE, equalityTolerance = 0) {
            return this.arcLength(this.angleAlong(start, end, tolerance, this.angularWidth(equalityTolerance)));
        }
        /**
         * Calculates the true bearing along this circle at a point on the circle.
         * @param point A point on this circle.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `point` lies on this circle. Defaults
         * to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns the bearing along this circle at the point.
         * @throws Error if the point does not lie on this circle.
         */
        bearingAt(point, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            if (!(point instanceof Float64Array)) {
                point = GeoPoint.sphericalToCartesian(point, GeoCircle.vec3Cache[1]);
            }
            if (tolerance < Math.PI && !this.includes(point, tolerance)) {
                throw new Error(`GeoCircle: the specified point does not lie on this circle (distance of ${Math.abs(this.distance(point))} vs tolerance of ${tolerance}).`);
            }
            if (this._sinRadius <= GeoCircle.ANGULAR_TOLERANCE || 1 - Math.abs(Vec3Math.dot(point, GeoCircle.NORTH_POLE)) <= GeoCircle.ANGULAR_TOLERANCE) {
                // Meaningful bearings cannot be defined along a circle with 0 radius (effectively a point) and at the north and south poles.
                return NaN;
            }
            const radialNormal = Vec3Math.normalize(Vec3Math.cross(this._center, point, GeoCircle.vec3Cache[2]), GeoCircle.vec3Cache[2]);
            const northNormal = Vec3Math.normalize(Vec3Math.cross(point, GeoCircle.NORTH_POLE, GeoCircle.vec3Cache[3]), GeoCircle.vec3Cache[3]);
            return (Vec3Math.unitAngle(radialNormal, northNormal) * (radialNormal[2] >= 0 ? 1 : -1) * Avionics.Utils.RAD2DEG - 90 + 360) % 360;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        offsetDistanceAlong(point, distance, out, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const angle = distance / Math.sin(this.radius);
            return this._offsetAngleAlong(point, angle, out, tolerance);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        offsetAngleAlong(point, angle, out, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            return this._offsetAngleAlong(point, angle, out, tolerance);
        }
        /**
         * Offsets a point on this circle by a specified angular distance. The direction of the offset for positive distances
         * is counterclockwise when viewed from above the center of this circle.
         * @param point The point to offset.
         * @param angle The angular distance by which to offset, in radians.
         * @param out A Float64Array or GeoPoint object to which to write the result.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `point` lies on this circle. Defaults
         * to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns The offset point.
         * @throws Error if the point does not lie on this circle.
         */
        _offsetAngleAlong(point, angle, out, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            if (!(point instanceof Float64Array)) {
                point = GeoPoint.sphericalToCartesian(point, GeoCircle.vec3Cache[3]);
            }
            if (tolerance < Math.PI && !this.includes(point, tolerance)) {
                throw new Error(`GeoCircle: the specified point does not lie on this circle (distance of ${Math.abs(this.distance(point))} vs tolerance of ${tolerance}).`);
            }
            if (this._sinRadius <= GeoCircle.ANGULAR_TOLERANCE) {
                // Offsetting any point on a circle of effectively zero radius (i.e. a point) will just yield the same point.
                return out instanceof GeoPoint ? out.setFromCartesian(this._center) : Vec3Math.copy(this._center, out);
            }
            // Since point may not lie exactly on this circle due to error tolerance, project point onto this circle to ensure
            // the offset point lies exactly on this circle.
            point = this.closest(point, GeoCircle.vec3Cache[3]);
            const sin = Math.sin(angle / 2);
            const q0 = Math.cos(angle / 2);
            const q1 = sin * this._center[0];
            const q2 = sin * this._center[1];
            const q3 = sin * this._center[2];
            const q0Sq = q0 * q0;
            const q1Sq = q1 * q1;
            const q2Sq = q2 * q2;
            const q3Sq = q3 * q3;
            const q01 = q0 * q1;
            const q02 = q0 * q2;
            const q03 = q0 * q3;
            const q12 = q1 * q2;
            const q13 = q1 * q3;
            const q23 = q2 * q3;
            const rot_11 = q0Sq + q1Sq - q2Sq - q3Sq;
            const rot_12 = 2 * (q12 - q03);
            const rot_13 = 2 * (q13 + q02);
            const rot_21 = 2 * (q12 + q03);
            const rot_22 = q0Sq - q1Sq + q2Sq - q3Sq;
            const rot_23 = 2 * (q23 - q01);
            const rot_31 = 2 * (q13 - q02);
            const rot_32 = 2 * (q23 + q01);
            const rot_33 = (q0Sq - q1Sq - q2Sq + q3Sq);
            const x = point[0];
            const y = point[1];
            const z = point[2];
            const rotX = rot_11 * x + rot_12 * y + rot_13 * z;
            const rotY = rot_21 * x + rot_22 * y + rot_23 * z;
            const rotZ = rot_31 * x + rot_32 * y + rot_33 * z;
            return out instanceof Float64Array
                ? Vec3Math.set(rotX, rotY, rotZ, out)
                : out.setFromCartesian(Vec3Math.set(rotX, rotY, rotZ, GeoCircle.vec3Cache[2]));
        }
        /**
         * Rotates this circle around a pivot.
         * @param pivot The pivot point of the rotation. If the pivot point does not lie exactly on this circle, then it will
         * be projected onto this circle.
         * @param angle The angle by which to rotate, in radians. Positive angles rotate the circle clockwise. In other
         * words, if vector A is parallel to this circle at the pivot point and vector B is parallel to this circle after a
         * rotation by angle `delta` at the pivot point, then vector B is equal to vector A after a clockwise rotation by
         * angle `delta`, as viewed from directly above the earth's surface at the pivot point.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `pivot` lies on this circle. Defaults
         * to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns This circle, after it has been rotated around the specified pivot point by the specified angle.
         * @throws Error if the pivot point does not lie on this circle, or if the pivot point cannot be projected onto this
         * circle because it is equidistant to all points on this circle.
         */
        rotate(pivot, angle, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            if (!(pivot instanceof Float64Array)) {
                pivot = GeoPoint.sphericalToCartesian(pivot, GeoCircle.vec3Cache[0]);
            }
            const pivotDistance = Math.abs(this.distance(pivot));
            if (tolerance < Math.PI && pivotDistance > tolerance) {
                throw new Error(`GeoCircle::rotate(): the specified pivot does not lie on this circle (distance of ${pivotDistance} vs tolerance of ${tolerance}).`);
            }
            const pivotToUse = pivotDistance <= Math.min(tolerance, GeoCircle.ANGULAR_TOLERANCE) ? pivot : this.closest(pivot, GeoCircle.vec3Cache[0]);
            if (!Vec3Math.isFinite(pivotToUse)) {
                throw new Error('GeoCircle::rotate(): the specified pivot cannot be projected onto this circle because it is equidistant to all points on the circle.');
            }
            if (Math.abs(angle) <= GeoCircle.ANGULAR_TOLERANCE || this._sinRadius <= GeoCircle.ANGULAR_TOLERANCE) {
                return this;
            }
            const center = Vec3Math.copy(this._center, GeoCircle.vec3Cache[5]);
            Vec3Math.copy(pivotToUse, this._center);
            this.offsetAngleAlong(center, -angle, this._center, Math.PI);
            return this;
        }
        /**
         * Calculates and returns the set of intersection points between this circle and another one, and writes the results
         * to an array of position vectors.
         * @param other The other circle to test for intersections.
         * @param out An array in which to store the results. The results will be stored at indexes 0 and 1. If these indexes
         * are empty, then new Float64Array objects will be created and inserted into the array.
         * @returns The number of solutions written to the out array. Either 0, 1, or 2.
         */
        intersection(other, out) {
            const center1 = this._center;
            const center2 = other._center;
            const radius1 = this._radius;
            const radius2 = other._radius;
            /**
             * Theory: We can model geo circles as the intersection between a sphere and the unit sphere (Earth's surface).
             * Therefore, the intersection of two geo circles is the intersection between two spheres AND the unit sphere.
             * First, we find the intersection of the two non-Earth spheres (which can either be a sphere, a circle, or a
             * point), then we find the intersection of that geometry with the unit sphere.
             */
            const dot = Vec3Math.dot(center1, center2);
            const dotSquared = dot * dot;
            if (dotSquared === 1) {
                // the two circles are concentric; either there are zero solutions or infinite solutions; either way we don't
                // write any solutions to the array.
                return 0;
            }
            // find the position vector to the center of the circle which defines the intersection of the two geo circle
            // spheres.
            const a = (Math.cos(radius1) - dot * Math.cos(radius2)) / (1 - dotSquared);
            const b = (Math.cos(radius2) - dot * Math.cos(radius1)) / (1 - dotSquared);
            const intersection = Vec3Math.add(Vec3Math.multScalar(center1, a, GeoCircle.vec3Cache[0]), Vec3Math.multScalar(center2, b, GeoCircle.vec3Cache[1]), GeoCircle.vec3Cache[0]);
            const intersectionLengthSquared = Vec3Math.dot(intersection, intersection);
            if (intersectionLengthSquared > 1) {
                // the two geo circle spheres do not intersect.
                return 0;
            }
            const cross = Vec3Math.cross(center1, center2, GeoCircle.vec3Cache[1]);
            const crossLengthSquared = Vec3Math.dot(cross, cross);
            if (crossLengthSquared === 0) {
                // this technically can't happen (since we already check if center1 dot center2 === +/-1 above, but just in
                // case...)
                return 0;
            }
            const offset = Math.sqrt((1 - intersectionLengthSquared) / crossLengthSquared);
            let solutionCount = 1;
            if (!out[0]) {
                out[0] = new Float64Array(3);
            }
            out[0].set(cross);
            Vec3Math.multScalar(out[0], offset, out[0]);
            Vec3Math.add(out[0], intersection, out[0]);
            if (offset > 0) {
                if (!out[1]) {
                    out[1] = new Float64Array(3);
                }
                out[1].set(cross);
                Vec3Math.multScalar(out[1], -offset, out[1]);
                Vec3Math.add(out[1], intersection, out[1]);
                solutionCount++;
            }
            return solutionCount;
        }
        /**
         * Calculates and returns the set of intersection points between this circle and another one, and writes the results
         * to an array of GeoPoint objects.
         * @param other The other circle to test for intersections.
         * @param out An array in which to store the results. The results will be stored at indexes 0 and 1. If these indexes
         * are empty, then new GeoPoint objects will be created and inserted into the array.
         * @returns The number of solutions written to the out array. Either 0, 1, or 2.
         */
        intersectionGeoPoint(other, out) {
            const solutionCount = this.intersection(other, GeoCircle.intersectionCache);
            for (let i = 0; i < solutionCount; i++) {
                if (!out[i]) {
                    out[i] = new GeoPoint(0, 0);
                }
                out[i].setFromCartesian(GeoCircle.intersectionCache[i]);
            }
            return solutionCount;
        }
        /**
         * Calculates and returns the number of intersection points between this circle and another one. Returns NaN if there
         * are an infinite number of intersection points.
         * @param other The other circle to test for intersections.
         * @param tolerance The error tolerance, in great-arc radians, of this operation. Defaults to
         * `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns the number of intersection points between this circle and the other one.
         */
        numIntersectionPoints(other, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const center1 = this.center;
            const center2 = other.center;
            const radius1 = this.radius;
            const radius2 = other.radius;
            const dot = Vec3Math.dot(center1, center2);
            const dotSquared = dot * dot;
            if (dotSquared === 1) {
                // the two circles are concentric; if they are the same circle there are an infinite number of intersections,
                // otherwise there are none.
                if (dot === 1) {
                    // centers are the same
                    return (Math.abs(this.radius - other.radius) <= tolerance) ? NaN : 0;
                }
                else {
                    // centers are antipodal
                    return (Math.abs(Math.PI - this.radius - other.radius) <= tolerance) ? NaN : 0;
                }
            }
            const a = (Math.cos(radius1) - dot * Math.cos(radius2)) / (1 - dotSquared);
            const b = (Math.cos(radius2) - dot * Math.cos(radius1)) / (1 - dotSquared);
            const intersection = Vec3Math.add(Vec3Math.multScalar(center1, a, GeoCircle.vec3Cache[0]), Vec3Math.multScalar(center2, b, GeoCircle.vec3Cache[1]), GeoCircle.vec3Cache[1]);
            const intersectionLengthSquared = Vec3Math.dot(intersection, intersection);
            if (intersectionLengthSquared > 1) {
                return 0;
            }
            const cross = Vec3Math.cross(center1, center2, GeoCircle.vec3Cache[1]);
            const crossLengthSquared = Vec3Math.dot(cross, cross);
            if (crossLengthSquared === 0) {
                return 0;
            }
            const sinTol = Math.sin(tolerance);
            return ((1 - intersectionLengthSquared) / crossLengthSquared > sinTol * sinTol) ? 2 : 1;
        }
        /**
         * Creates a new small circle from a lat/long coordinate pair and radius.
         * @param point The center of the new small circle.
         * @param radius The radius of the new small circle, in great-arc radians.
         * @returns a small circle.
         */
        static createFromPoint(point, radius) {
            return new GeoCircle(GeoPoint.sphericalToCartesian(point, GeoCircle.vec3Cache[0]), radius);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static createGreatCircle(arg1, arg2) {
            return new GeoCircle(GeoCircle._getGreatCircleNormal(arg1, arg2, GeoCircle.vec3Cache[0]), Math.PI / 2);
        }
        /**
         * Creates a new great circle defined by one point and a bearing offset. The new great circle will be equivalent to
         * the path projected from the point with the specified initial bearing (forward azimuth).
         * @param point A point that lies on the new great circle.
         * @param bearing The initial bearing from the point.
         * @returns a great circle.
         */
        static createGreatCircleFromPointBearing(point, bearing) {
            return new GeoCircle(GeoCircle.getGreatCircleNormalFromPointBearing(point, bearing, GeoCircle.vec3Cache[0]), Math.PI / 2);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static getGreatCircleNormal(arg1, arg2, out) {
            return GeoCircle._getGreatCircleNormal(arg1, arg2, out);
        }
        /**
         * Calculates a normal vector for a great circle given certain parameters.
         * @param arg1 A point that lies on the great circle.
         * @param arg2 A second point that lies on the great circle, or an initial bearing from the first point, or a
         * GeoCircle that is tangent and parallel to the great circle at the first point.
         * @param out The vector to which to write the result.
         * @returns The normal vector for the great circle with the specified parameters.
         */
        static _getGreatCircleNormal(arg1, arg2, out) {
            if (typeof arg2 === 'number') {
                return GeoCircle.getGreatCircleNormalFromPointBearing(arg1, arg2, out);
            }
            else if (arg2 instanceof GeoCircle) {
                return GeoCircle.getGreatCircleNormalFromCircle(arg1, arg2, out);
            }
            else {
                return GeoCircle.getGreatCircleNormalFromPoints(arg1, arg2, out);
            }
        }
        /**
         * Calculates a normal vector for a great circle that includes two given points and is parallel to the path from the
         * first point to the second point. If the two points are coincident or antipodal, then `NaN` will be written to all
         * components of the result.
         * @param point1 The first point that lies on the great circle.
         * @param point2 The second point that lies on the great circle.
         * @param out The vector to which to write the result.
         * @returns The normal vector for the great circle that includes the two specified points and is parallel to the path
         * from the first point to the second point.
         */
        static getGreatCircleNormalFromPoints(point1, point2, out) {
            if (!(point1 instanceof Float64Array)) {
                point1 = GeoPoint.sphericalToCartesian(point1, GeoCircle.vec3Cache[0]);
            }
            if (!(point2 instanceof Float64Array)) {
                point2 = GeoPoint.sphericalToCartesian(point2, GeoCircle.vec3Cache[1]);
            }
            return Vec3Math.normalize(Vec3Math.cross(point1, point2, out), out);
        }
        /**
         * Calculates a normal vector for a great circle that includes a given a point and is parallel to an initial bearing
         * at the same point.
         * @param point A point that lies on the great circle.
         * @param bearing The initial bearing from the point.
         * @param out The vector to which to write the result.
         * @returns The normal vector for the great circle that includes the specified point and is parallel to the specified
         * bearing at that point.
         */
        static getGreatCircleNormalFromPointBearing(point, bearing, out) {
            if (point instanceof Float64Array) {
                point = GeoCircle.geoPointCache[0].setFromCartesian(point);
            }
            const lat = point.lat * Avionics.Utils.DEG2RAD;
            const long = point.lon * Avionics.Utils.DEG2RAD;
            bearing *= Avionics.Utils.DEG2RAD;
            const sinLat = Math.sin(lat);
            const sinLon = Math.sin(long);
            const cosLon = Math.cos(long);
            const sinBearing = Math.sin(bearing);
            const cosBearing = Math.cos(bearing);
            const x = sinLon * cosBearing - sinLat * cosLon * sinBearing;
            const y = -cosLon * cosBearing - sinLat * sinLon * sinBearing;
            const z = Math.cos(lat) * sinBearing;
            return Vec3Math.set(x, y, z, out);
        }
        /**
         * Calculates a normal vector for a great circle that is tangent and parallel to a GeoCircle at a given point. If the
         * specified point does not lie exactly on the GeoCircle, then the projection of the point onto the GeoCircle will be
         * used instead. If the point cannot be projected onto the GeoCircle, then `NaN` will be written to all components of
         * the result.
         * @param point The point at which the great circle whose normal vector to calculate is tangent and parallel to the
         * GeoCircle.
         * @param circle A GeoCircle.
         * @param out The vector to which to write the result.
         * @returns The normal vector for the great circle that is tangent and parallel to the specified GeoCircle at the
         * specified point.
         */
        static getGreatCircleNormalFromCircle(point, circle, out) {
            const projectedPoint = circle.closest(point, GeoCircle.vec3Cache[0]);
            if (!Vec3Math.isFinite(projectedPoint)) {
                return Vec3Math.set(NaN, NaN, NaN, out);
            }
            if (circle.isGreatCircle()) {
                return Vec3Math.copy(circle.center, out);
            }
            return Vec3Math.normalize(Vec3Math.cross(Vec3Math.cross(projectedPoint, circle._center, out), projectedPoint, out), out);
        }
    }
    /** The default angular tolerance used by `GeoCircle`, in radians. */
    GeoCircle.ANGULAR_TOLERANCE = GeoMath.ANGULAR_TOLERANCE;
    GeoCircle.NORTH_POLE = Vec3Math.create(0, 0, 1);
    GeoCircle.geoPointCache = [new GeoPoint(0, 0)];
    GeoCircle.vec3Cache = ArrayUtils.create(6, () => Vec3Math.create());
    GeoCircle.intersectionCache = [Vec3Math.create(), Vec3Math.create()];

    /**
     * Navigational mathematics functions.
     */
    class NavMath {
        /**
         * Clamps a value to a min and max.
         * @param val The value to clamp.
         * @param min The minimum value to clamp to.
         * @param max The maximum value to clamp to.
         * @returns The clamped value.
         */
        static clamp(val, min, max) {
            return Math.min(Math.max(val, min), max);
        }
        /**
         * Normalizes a heading to a 0-360 range.
         * @param heading The heading to normalize.
         * @returns The normalized heading.
         */
        static normalizeHeading(heading) {
            if (isFinite(heading)) {
                return (heading % 360 + 360) % 360;
            }
            else {
                console.error(`normalizeHeading: Invalid heading: ${heading}`);
                return NaN;
            }
        }
        /**
         * Inverts a heading value by adding 180 and normalizing.
         * @param heading The heading to invert/reciprocate.
         * @returns The inverted/reciprocated heading.
         * */
        static reciprocateHeading(heading) {
            return NavMath.normalizeHeading(heading + 180);
        }
        /**
         * Gets the turn radius for a given true airspeed.
         * @param airspeedTrue The true airspeed of the plane, in knots.
         * @param bankAngle The bank angle of the plane, in degrees.
         * @returns The airplane turn radius, in meters.
         */
        static turnRadius(airspeedTrue, bankAngle) {
            return (Math.pow(airspeedTrue, 2) / (11.26 * Math.tan(bankAngle * Avionics.Utils.DEG2RAD)))
                / 3.2808399;
        }
        /**
         * Gets the required bank angle for a given true airspeed and turn radius.
         * @param airspeedTrue The true airspeed of the plane, in knots.
         * @param radius The airplane turn radius, in meters.
         * @returns The required bank angle, in degrees.
         */
        static bankAngle(airspeedTrue, radius) {
            const airspeedMS = airspeedTrue * 0.51444444;
            return Math.atan(Math.pow(airspeedMS, 2) / (radius * 9.80665)) * Avionics.Utils.RAD2DEG;
        }
        /**
         * Calculates the expected ground track of an airplane for a given heading, true airspeed, and wind conditions.
         * @param heading The airplane's heading, in degrees.
         * @param tas The airplane's true airspeed, in the same units as `windSpeed`.
         * @param windDirection The wind direction, in degrees. Wind direction is defined as the heading **from** which the
         * wind is blowing.
         * @param windSpeed The wind speed, in the same units as `tas`.
         * @returns The expected ground track of an airplane, in degrees, for the specified heading, true airspeed, and wind
         * conditions. If the specified parameters result in an expected ground speed of zero, then `NaN` is returned.
         */
        static headingToGroundTrack(heading, tas, windDirection, windSpeed) {
            if (windSpeed === 0) {
                return heading;
            }
            const x = tas * Math.cos(heading * Avionics.Utils.DEG2RAD) - windSpeed * Math.cos(windDirection * Avionics.Utils.DEG2RAD);
            const y = tas * Math.sin(heading * Avionics.Utils.DEG2RAD) - windSpeed * Math.sin(windDirection * Avionics.Utils.DEG2RAD);
            if (x === 0 && y === 0) {
                return NaN;
            }
            return MathUtils.normalizeAngleDeg(Math.atan2(y, x) * Avionics.Utils.RAD2DEG);
        }
        /**
         * Get the turn direction for a given course change.
         * @param startCourse The start course.
         * @param endCourse The end course.
         * @returns The turn direction for the course change.
         */
        static getTurnDirection(startCourse, endCourse) {
            return NavMath.normalizeHeading(endCourse - startCourse) > 180 ? 'left' : 'right';
        }
        /**
         * Converts polar radians to degrees north.
         * @param radians The radians to convert.
         * @returns The angle, in degrees north.
         */
        static polarToDegreesNorth(radians) {
            return NavMath.normalizeHeading((180 / Math.PI) * (Math.PI / 2 - radians));
        }
        /**
         * Converts degrees north to polar radians.
         * @param degrees The degrees to convert.
         * @returns The angle radians, in polar.
         */
        static degreesNorthToPolar(degrees) {
            return NavMath.normalizeHeading(degrees - 90) / (180 / Math.PI);
        }
        /**
         * Calculates the distance along an arc on Earth's surface. The arc begins at the intersection of the great circle
         * passing through the center of a circle of radius `radius` meters in the direction of 'startBearing', and ends at
         * the intersection of the great circle passing through the center of the circle in the direction of 'endBearing',
         * proceeding clockwise (as viewed from above).
         * @param startBearing The degrees of the start of the arc.
         * @param endBearing The degrees of the end of the arc.
         * @param radius The radius of the arc, in meters.
         * @returns The arc distance.
         */
        static calculateArcDistance(startBearing, endBearing, radius) {
            const angularWidth = ((endBearing - startBearing + 360) % 360) * Avionics.Utils.DEG2RAD;
            const conversion = UnitType.GA_RADIAN.convertTo(1, UnitType.METER);
            return angularWidth * Math.sin(radius / conversion) * conversion;
        }
        /**
         * Calculates the intersection of a line and a circle.
         * @param x1 The start x of the line.
         * @param y1 The start y of the line.
         * @param x2 The end x of the line.
         * @param y2 The end y of the line.
         * @param cx The circle center x.
         * @param cy The circle center y.
         * @param r The radius of the circle.
         * @param sRef The reference to the solution object to write the solution to.
         * @returns The number of solutions (0, 1 or 2).
         */
        static circleIntersection(x1, y1, x2, y2, cx, cy, r, sRef) {
            const dx = x2 - x1;
            const dy = y2 - y1;
            const a = dx * dx + dy * dy;
            const b = 2 * (dx * (x1 - cx) + dy * (y1 - cy));
            const c = (x1 - cx) * (x1 - cx) + (y1 - cy) * (y1 - cy) - r * r;
            const det = b * b - 4 * a * c;
            if (a < 0.0000001 || det < 0) {
                sRef.x1 = NaN;
                sRef.x2 = NaN;
                sRef.y1 = NaN;
                sRef.y2 = NaN;
                return 0;
            }
            else if (det == 0) {
                const t = -b / (2 * a);
                sRef.x1 = x1 + t * dx;
                sRef.y1 = y1 + t * dy;
                sRef.x2 = NaN;
                sRef.y2 = NaN;
                return 1;
            }
            else {
                const t1 = ((-b + Math.sqrt(det)) / (2 * a));
                sRef.x1 = x1 + t1 * dx;
                sRef.y1 = y1 + t1 * dy;
                const t2 = ((-b - Math.sqrt(det)) / (2 * a));
                sRef.x2 = x1 + t2 * dx;
                sRef.y2 = y1 + t2 * dy;
                return 2;
            }
        }
        /**
         * Gets the degrees north that a point lies on a circle.
         * @param cx The x point of the center of the circle.
         * @param cy The y point of the center of the circle.
         * @param x The x point to get the bearing for.
         * @param y The y point to get the bearing for.
         * @returns The angle in degrees north that the point is relative to the center.
         */
        static northAngle(cx, cy, x, y) {
            return NavMath.polarToDegreesNorth(Math.atan2(y - cy, x - cx));
        }
        /**
         * Checks if a degrees north bearing is between two other degrees north bearings.
         * @param bearing The bearing in degrees north to check.
         * @param start The start bearing in degrees north.
         * @param end The end bearing, in degrees north.
         * @returns True if the bearing is between the two provided bearings, false otherwise.
         */
        static bearingIsBetween(bearing, start, end) {
            const range = this.normalizeHeading(end - start);
            const relativeBearing = this.normalizeHeading(bearing - start);
            return relativeBearing >= 0 && relativeBearing <= range;
        }
        /**
         * Converts a degrees north heading to a degrees north turn circle angle.
         * @param heading The heading to convert.
         * @param turnDirection The direction of the turn.
         * @returns A degrees north turn circle angle.
         */
        static headingToAngle(heading, turnDirection) {
            return NavMath.normalizeHeading(heading + (turnDirection === 'left' ? 90 : -90));
        }
        /**
         * Converts a degrees north turn circle angle to a degrees north heading.
         * @param angle The turn circle angle to convert.
         * @param turnDirection The direction of the turn.
         * @returns A degrees north heading.
         */
        static angleToHeading(angle, turnDirection) {
            return NavMath.normalizeHeading(angle + (turnDirection === 'left' ? -90 : 90));
        }
        /**
         * Calculates the wind correction angle.
         * @param course The current plane true course.
         * @param airspeedTrue The current plane true airspeed.
         * @param windDirection The direction of the wind, in degrees true.
         * @param windSpeed The current speed of the wind.
         * @returns The calculated wind correction angle.
         */
        static windCorrectionAngle(course, airspeedTrue, windDirection, windSpeed) {
            const currCrosswind = windSpeed * (Math.sin((course * Math.PI / 180) - (windDirection * Math.PI / 180)));
            const windCorrection = 180 * Math.asin(currCrosswind / airspeedTrue) / Math.PI;
            return windCorrection;
        }
        /**
         * Calculates the cross track deviation from the provided leg fixes.
         * @param start The location of the starting fix of the leg.
         * @param end The location of the ending fix of the leg.
         * @param pos The current plane location coordinates.
         * @returns The amount of cross track deviation, in nautical miles.
         */
        static crossTrack(start, end, pos) {
            const path = NavMath.geoCircleCache[0].setAsGreatCircle(start, end);
            if (isNaN(path.center[0])) {
                return NaN;
            }
            return UnitType.GA_RADIAN.convertTo(path.distance(pos), UnitType.NMILE);
        }
        /**
         * Calculates the along-track distance from a starting point to another point along a great-circle track running
         * through the starting point.
         * @param start The start of the great-circle track.
         * @param end The end of the great-circle track.
         * @param pos The point for which to calculate the along-track distance.
         * @returns The along-track distance, in nautical miles.
         */
        static alongTrack(start, end, pos) {
            const path = NavMath.geoCircleCache[0].setAsGreatCircle(start, end);
            if (isNaN(path.center[0])) {
                return NaN;
            }
            const distance = path.distanceAlong(start, path.closest(pos, NavMath.vec3Cache[0]));
            return UnitType.GA_RADIAN.convertTo((distance + Math.PI) % (2 * Math.PI) - Math.PI, UnitType.NMILE);
        }
        /**
         * Calculates the desired track from the provided leg fixes.
         * @param start The location of the starting fix of the leg.
         * @param end The location of the ending fix of the leg.
         * @param pos The current plane location coordinates.
         * @returns The desired track, in degrees true.
         */
        static desiredTrack(start, end, pos) {
            const path = NavMath.geoCircleCache[0].setAsGreatCircle(start, end);
            if (isNaN(path.center[0])) {
                return NaN;
            }
            return path.bearingAt(path.closest(pos, NavMath.vec3Cache[0]));
        }
        /**
         * Gets the desired track for a given arc.
         * @param center The center of the arc.
         * @param turnDirection The direction of the turn.
         * @param pos The current plane position.
         * @returns The desired track.
         */
        static desiredTrackArc(center, turnDirection, pos) {
            const northAngle = NavMath.geoPointCache[0].set(pos).bearingFrom(center);
            //TODO: Clamp the arc angle to the start and end angles
            return NavMath.angleToHeading(northAngle, turnDirection);
        }
        /**
         * Gets the percentage along the arc path that the plane currently is.
         * @param start The start of the arc, in degrees north.
         * @param end The end of the arc, in degrees north.
         * @param center The center location of the arc.
         * @param turnDirection The direction of the turn.
         * @param pos The current plane position.
         * @returns The percentage along the arc the plane is.
         */
        static percentAlongTrackArc(start, end, center, turnDirection, pos) {
            const bearingFromCenter = NavMath.geoPointCache[0].set(center).bearingTo(pos);
            const sign = turnDirection === 'right' ? 1 : -1;
            const alpha = ((end - start) * sign + 360) % 360;
            const mid = (start + alpha / 2 * sign + 360) % 360;
            const rotBearing = ((bearingFromCenter - mid) + 540) % 360 - 180;
            const frac = rotBearing * sign / alpha + 0.5;
            return frac;
        }
        /**
         * Gets a position given an arc and a distance from the arc start.
         * @param start The start bearing of the arc.
         * @param center The center of the arc.
         * @param radius The radius of the arc.
         * @param turnDirection The turn direction for the arc.
         * @param distance The distance along the arc to get the position for.
         * @param out The position to write to.
         * @returns The position along the arc that was written to.
         */
        static positionAlongArc(start, center, radius, turnDirection, distance, out) {
            const convertedRadius = UnitType.GA_RADIAN.convertTo(Math.sin(UnitType.METER.convertTo(radius, UnitType.GA_RADIAN)), UnitType.METER);
            const theta = UnitType.RADIAN.convertTo(distance / convertedRadius, UnitType.DEGREE);
            const bearing = turnDirection === 'right' ? start + theta : start - theta;
            center.offset(NavMath.normalizeHeading(bearing), UnitType.METER.convertTo(radius, UnitType.GA_RADIAN), out);
            return out;
        }
        /**
         * Gets the cross track distance for a given arc.
         * @param center The center of the arc.
         * @param radius The radius of the arc, in meters.
         * @param pos The current plane position.
         * @returns The cross track distance, in NM.
         */
        static crossTrackArc(center, radius, pos) {
            return UnitType.METER.convertTo(radius, UnitType.NMILE) - UnitType.GA_RADIAN.convertTo(NavMath.geoPointCache[0].set(pos).distance(center), UnitType.NMILE);
        }
        /**
         * Gets the total difference in degrees between two angles.
         * @param a The first angle.
         * @param b The second angle.
         * @returns The difference between the two angles, in degrees, in range -180°...+180°.
         */
        static diffAngle(a, b) {
            let diff = b - a;
            while (diff > 180) {
                diff -= 360;
            }
            while (diff <= -180) {
                diff += 360;
            }
            return diff;
        }
        /**
         * Finds side a given sides b, c, and angles beta, gamma.
         * @param b The length of side b, as a trigonometric ratio.
         * @param c The length of side c, as a trigonometric ratio.
         * @param beta The angle, in radians, of the opposite of side b.
         * @param gamma The angle, in radians, of the opposite of side c
         * @returns The length of side a, as a trigonometric ratio.
         */
        static napierSide(b, c, beta, gamma) {
            return 2 * Math.atan(Math.tan(0.5 * (b - c))
                * (Math.sin(0.5 * (beta + gamma)) / Math.sin(0.5 * (beta - gamma))));
        }
        /**
         * Calculates a normal vector to a provided course in degrees north.
         * @param course The course in degrees north.
         * @param turnDirection The direction of the turn to orient the normal.
         * @param outVector The normal vector for the provided course.
         */
        static normal(course, turnDirection, outVector) {
            const normalCourse = NavMath.headingToAngle(course, turnDirection);
            const polarCourse = NavMath.degreesNorthToPolar(normalCourse);
            outVector[0] = Math.cos(polarCourse);
            outVector[1] = Math.sin(polarCourse);
        }
    }
    NavMath.vec3Cache = [new Float64Array(3)];
    NavMath.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
    NavMath.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /// <reference types="@microsoft/msfs-types/coherent/facilities" />
    /**
     * A utility class for working with magnetic variation (magnetic declination).
     */
    class MagVar {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static get(arg1, arg2) {
            return MagVar.getMagVar(arg1, arg2);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static magneticToTrue(bearing, arg1, arg2) {
            return NavMath.normalizeHeading(bearing + (typeof arg1 === 'number' && arg2 === undefined ? arg1 : MagVar.getMagVar(arg1, arg2)));
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static trueToMagnetic(bearing, arg1, arg2) {
            return NavMath.normalizeHeading(bearing - (typeof arg1 === 'number' && arg2 === undefined ? arg1 : MagVar.getMagVar(arg1, arg2)));
        }
        /**
         * Gets the magnetic variation (magnetic declination) at a specific point on Earth.
         * @param arg1 The query point, or the latitude of the query point.
         * @param arg2 The longitude of the query point.
         * @returns The magnetic variation (magnetic declination) at the point.
         */
        static getMagVar(arg1, arg2) {
            if (typeof Facilities === 'undefined') {
                // In case this code is executed before the Facilities class is created.
                return 0;
            }
            let lat, lon;
            if (typeof arg1 === 'number') {
                lat = arg1;
                lon = arg2;
            }
            else {
                lat = arg1.lat;
                lon = arg1.lon;
            }
            return Facilities.getMagVar(lat, lon);
        }
    }

    /**
     * A publisher for AHRS information.
     */
    class AhrsPublisher extends SimVarPublisher {
        /**
         * Creates an AhrsPublisher.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            var _a;
            super([
                ['pitch_deg', { name: 'ATTITUDE INDICATOR PITCH DEGREES:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
                ['roll_deg', { name: 'ATTITUDE INDICATOR BANK DEGREES:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
                ['hdg_deg', { name: 'HEADING INDICATOR:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
                ['hdg_deg_true', { name: 'HEADING INDICATOR:#index#', type: exports.SimVarValueType.Degree, map: (heading) => MagVar.magneticToTrue(heading, this.magVar), indexed: true }],
                ['delta_heading_rate', { name: 'DELTA HEADING RATE:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
                ['turn_coordinator_ball', { name: 'TURN COORDINATOR BALL', type: exports.SimVarValueType.Number }],
                ['actual_hdg_deg', { name: 'PLANE HEADING DEGREES MAGNETIC', type: exports.SimVarValueType.Degree }],
                ['actual_hdg_deg_true', { name: 'PLANE HEADING DEGREES TRUE', type: exports.SimVarValueType.Degree }],
                ['actual_pitch_deg', { name: 'PLANE PITCH DEGREES', type: exports.SimVarValueType.Degree }],
                ['actual_roll_deg', { name: 'PLANE BANK DEGREES', type: exports.SimVarValueType.Degree }],
            ], bus, pacer);
            this.registeredSimVarIds = {
                magVar: SimVar.GetRegisteredId('MAGVAR', exports.SimVarValueType.Degree, ''),
            };
            this.magVar = 0;
            (_a = this.needUpdateMagVar) !== null && _a !== void 0 ? _a : (this.needUpdateMagVar = false);
        }
        /** @inheritdoc */
        onTopicSubscribed(topic) {
            super.onTopicSubscribed(topic);
            if (topic.startsWith('hdg_deg_true')) {
                this.needUpdateMagVar = true;
            }
        }
        /** @inheritdoc */
        onUpdate() {
            if (this.needUpdateMagVar) {
                this.magVar = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.magVar);
            }
            super.onUpdate();
        }
    }

    /**
     * A publisher for events related to the sim's AI piloting feature.
     */
    class AiPilotPublisher extends SimVarPublisher {
        /**
         * Creates a new instance of AiPilotPublisher.
         * @param bus The event bus to which to publish.
         */
        constructor(bus) {
            super(new Map([
                ['ai_delegate_controls_active', { name: 'DELEGATE CONTROLS TO AI', type: exports.SimVarValueType.Bool }],
                ['ai_auto_rudder_active', { name: 'AUTO COORDINATION', type: exports.SimVarValueType.Bool }]
            ]), bus);
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A publisher for Aircraft information.
     */
    class AircraftInertialPublisher extends SimVarPublisher {
        /**
         * Create a AircraftSimvarPublisher.
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer = undefined) {
            const simvars = new Map([
                ['acceleration_body_x', { name: 'ACCELERATION BODY X', type: exports.SimVarValueType.MetersPerSecondSquared }],
                ['acceleration_body_y', { name: 'ACCELERATION BODY Y', type: exports.SimVarValueType.MetersPerSecondSquared }],
                ['acceleration_body_z', { name: 'ACCELERATION BODY Z', type: exports.SimVarValueType.MetersPerSecondSquared }],
                ['rotation_velocity_body_x', { name: 'ROTATION VELOCITY BODY X', type: exports.SimVarValueType.DegreesPerSecond }],
                ['rotation_velocity_body_y', { name: 'ROTATION VELOCITY BODY Y', type: exports.SimVarValueType.DegreesPerSecond }],
                ['rotation_velocity_body_z', { name: 'ROTATION VELOCITY BODY Z', type: exports.SimVarValueType.DegreesPerSecond }],
                ['load_factor', { name: 'SEMIBODY LOADFACTOR Y', type: exports.SimVarValueType.Number }],
                ['load_factor_rate', { name: 'SEMIBODY LOADFACTOR YDOT', type: exports.SimVarValueType.PerSecond }],
            ]);
            super(simvars, bus, pacer);
        }
        /** @inheritdoc */
        onUpdate() {
            super.onUpdate();
        }
    }

    /**
     * Ambient precipitation states.
     */
    exports.AmbientPrecipState = void 0;
    (function (AmbientPrecipState) {
        AmbientPrecipState[AmbientPrecipState["None"] = 2] = "None";
        AmbientPrecipState[AmbientPrecipState["Rain"] = 4] = "Rain";
        AmbientPrecipState[AmbientPrecipState["Snow"] = 8] = "Snow";
    })(exports.AmbientPrecipState || (exports.AmbientPrecipState = {}));
    /**
     * A publisher for ambient environment information.
     */
    class AmbientPublisher extends SimVarPublisher {
        /**
         * Creates an AmbientPublisher.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            super([
                ['ambient_precip_state', { name: 'AMBIENT PRECIP STATE', type: exports.SimVarValueType.Number }],
                ['ambient_precip_rate', { name: 'AMBIENT PRECIP RATE', type: exports.SimVarValueType.MillimetersWater }],
                ['ambient_visibility', { name: 'AMBIENT VISIBILITY', type: exports.SimVarValueType.Meters }],
                ['ambient_in_cloud', { name: 'AMBIENT IN CLOUD', type: exports.SimVarValueType.Bool }],
                ['ambient_qnh_inhg', { name: 'SEA LEVEL PRESSURE', type: exports.SimVarValueType.InHG }],
                ['ambient_qnh_mb', { name: 'SEA LEVEL PRESSURE', type: exports.SimVarValueType.MB }],
                ['ambient_light_intensity', { name: 'AMBIENT LIGHT SENSOR', type: exports.SimVarValueType.Number }],
            ], bus, pacer);
        }
    }

    /**
     * A publisher for anti-ice system information.
     */
    class AntiIcePublisher extends SimVarPublisher {
        /**
         * Creates an instance of an AntiIcePublisher.
         * @param bus The event bus to use with this instance.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            const engineIndexedSimVars = [
                ['anti_ice_engine_switch_on', { name: 'ENG ANTI ICE', type: exports.SimVarValueType.Bool }],
                ['anti_ice_prop_switch_on', { name: 'PROP DEICE SWITCH', type: exports.SimVarValueType.Bool }]
            ];
            const simvars = new Map(AntiIcePublisher.nonIndexedSimVars);
            // add engine-indexed simvars
            const engineCount = SimVar.GetSimVarValue('NUMBER OF ENGINES', exports.SimVarValueType.Number);
            for (const [topic, simvar] of engineIndexedSimVars) {
                for (let i = 1; i <= engineCount; i++) {
                    simvars.set(`${topic}_${i}`, {
                        name: `${simvar.name}:${i}`,
                        type: simvar.type,
                        map: simvar.map
                    });
                }
            }
            super(simvars, bus, pacer);
        }
    }
    AntiIcePublisher.nonIndexedSimVars = [
        ['anti_ice_structural_switch_on', { name: 'STRUCTURAL DEICE SWITCH', type: exports.SimVarValueType.Bool }],
        ['anti_ice_windshield_switch_on', { name: 'WINDSHIELD DEICE SWITCH', type: exports.SimVarValueType.Bool }],
        ['anti_ice_structural_ice_pct', { name: 'STRUCTURAL ICE PCT', type: exports.SimVarValueType.Percent }]
    ];

    exports.APLockType = void 0;
    (function (APLockType) {
        APLockType[APLockType["Heading"] = 0] = "Heading";
        APLockType[APLockType["Nav"] = 1] = "Nav";
        APLockType[APLockType["Alt"] = 2] = "Alt";
        APLockType[APLockType["Bank"] = 3] = "Bank";
        APLockType[APLockType["WingLevel"] = 4] = "WingLevel";
        APLockType[APLockType["Vs"] = 5] = "Vs";
        APLockType[APLockType["Flc"] = 6] = "Flc";
        APLockType[APLockType["Pitch"] = 7] = "Pitch";
        APLockType[APLockType["Approach"] = 8] = "Approach";
        APLockType[APLockType["Backcourse"] = 9] = "Backcourse";
        APLockType[APLockType["Glideslope"] = 10] = "Glideslope";
        APLockType[APLockType["VNav"] = 11] = "VNav";
    })(exports.APLockType || (exports.APLockType = {}));
    /** base publisher for simvars */
    class APSimVarPublisher extends SimVarPublisher {
        /**
         * Create an APSimVarPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the pace of publishing
         */
        constructor(bus, pacer = undefined) {
            super(APSimVarPublisher.simvars, bus, pacer);
        }
    }
    APSimVarPublisher.simvars = new Map([
        ['ap_master_status', { name: 'AUTOPILOT MASTER', type: exports.SimVarValueType.Bool }],
        ['ap_yd_status', { name: 'AUTOPILOT YAW DAMPER', type: exports.SimVarValueType.Bool }],
        ['ap_disengage_status', { name: 'AUTOPILOT DISENGAGED', type: exports.SimVarValueType.Bool }],
        ['ap_heading_hold', { name: 'AUTOPILOT HEADING LOCK', type: exports.SimVarValueType.Bool }],
        ['ap_nav_hold', { name: 'AUTOPILOT NAV1 LOCK', type: exports.SimVarValueType.Bool }],
        ['ap_bank_hold', { name: 'AUTOPILOT BANK HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_max_bank_id', { name: 'AUTOPILOT MAX BANK ID', type: exports.SimVarValueType.Number }],
        ['ap_max_bank_value', { name: 'AUTOPILOT MAX BANK', type: exports.SimVarValueType.Degree }],
        ['ap_wing_lvl_hold', { name: 'AUTOPILOT WING LEVELER', type: exports.SimVarValueType.Bool }],
        ['ap_approach_hold', { name: 'AUTOPILOT APPROACH HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_backcourse_hold', { name: 'AUTOPILOT BACKCOURSE HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_vs_hold', { name: 'AUTOPILOT VERTICAL HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_flc_hold', { name: 'AUTOPILOT FLIGHT LEVEL CHANGE', type: exports.SimVarValueType.Bool }],
        ['ap_alt_hold', { name: 'AUTOPILOT ALTITUDE LOCK', type: exports.SimVarValueType.Bool }],
        ['ap_glideslope_hold', { name: 'AUTOPILOT GLIDESLOPE HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_pitch_hold', { name: 'AUTOPILOT PITCH HOLD', type: exports.SimVarValueType.Bool }],
        ['ap_toga_hold', { name: 'AUTOPILOT TAKEOFF POWER ACTIVE', type: exports.SimVarValueType.Bool }],
        ['ap_heading_selected', { name: 'AUTOPILOT HEADING LOCK DIR:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
        ['ap_altitude_selected', { name: 'AUTOPILOT ALTITUDE LOCK VAR:#index#', type: exports.SimVarValueType.Feet, indexed: true }],
        ['ap_pitch_selected', { name: 'AUTOPILOT PITCH HOLD REF', type: exports.SimVarValueType.Degree }],
        ['ap_vs_selected', { name: 'AUTOPILOT VERTICAL HOLD VAR:#index#', type: exports.SimVarValueType.FPM, indexed: true }],
        ['ap_fpa_selected', { name: 'L:WT_AP_FPA_Target:#index#', type: exports.SimVarValueType.Degree, indexed: true }],
        ['ap_ias_selected', { name: 'AUTOPILOT AIRSPEED HOLD VAR:#index#', type: exports.SimVarValueType.Knots, indexed: true }],
        ['ap_mach_selected', { name: 'AUTOPILOT MACH HOLD VAR:#index#', type: exports.SimVarValueType.Number, indexed: true }],
        ['ap_selected_speed_is_mach', { name: 'AUTOPILOT MANAGED SPEED IN MACH', type: exports.SimVarValueType.Bool }],
        ['ap_selected_speed_is_manual', { name: 'L:XMLVAR_SpeedIsManuallySet', type: exports.SimVarValueType.Bool }],
        ['flight_director_bank', { name: 'AUTOPILOT FLIGHT DIRECTOR BANK', type: exports.SimVarValueType.Degree }],
        ['flight_director_pitch', { name: 'AUTOPILOT FLIGHT DIRECTOR PITCH', type: exports.SimVarValueType.Degree }],
        ['flight_director_is_active_1', { name: 'AUTOPILOT FLIGHT DIRECTOR ACTIVE:1', type: exports.SimVarValueType.Bool }],
        ['flight_director_is_active_2', { name: 'AUTOPILOT FLIGHT DIRECTOR ACTIVE:2', type: exports.SimVarValueType.Bool }],
        ['vnav_active', { name: 'L:XMLVAR_VNAVButtonValue', type: exports.SimVarValueType.Bool }]
    ]);
    /**
     * Publishes autopilot data
     */
    class AutopilotPublisher extends BasePublisher {
        /**
         * Creates an AutopilotPublisher
         * @param bus The event bus to publish to.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            super(bus, pacer);
        }
        /**
         * Publish an AP master engage event
         */
        publishMasterEngage() {
            this.publish('ap_master_on', true);
        }
        /**
         * Publish an AP master disengage event
         */
        publishMasterDisengage() {
            this.publish('ap_master_off', true);
        }
        /**
         * Publish a YD engage event
         */
        publishYdEngage() {
            this.publish('ap_yd_on', true);
        }
        /**
         * Publish a YD disengage event
         */
        publishYdDisengage() {
            this.publish('ap_yd_off', true);
        }
        /**
         * Publish a lock set event
         * @param lock The lock/hold set
         */
        publishLockSet(lock) {
            this.publish('ap_lock_set', lock);
        }
        /**
         * Publish a lock release event
         * @param lock The lock/hold released
         */
        publishLockRelease(lock) {
            this.publish('ap_lock_release', lock);
        }
    }
    /**
     * Manages an autopilot system
     */
    class AutopilotInstrument {
        /**
         * Create an AutopilotInstrument
         * @param bus The event bus to publish to
         */
        constructor(bus) {
            this.bus = bus;
            // this.hEvents = this.bus.getSubscriber<HEvent>();
            this.publisher = new AutopilotPublisher(bus);
            this.simVarPublisher = new APSimVarPublisher(bus);
            this.simVarSubscriber = new EventSubscriber(bus);
        }
        /**
         * Initialize the instrument
         */
        init() {
            this.publisher.startPublish();
            this.simVarPublisher.startPublish();
            // console.log('initting autopilot');
            this.simVarSubscriber.on('ap_master_status').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishMasterEngage();
                }
                else {
                    this.publisher.publishMasterDisengage();
                }
            });
            this.simVarSubscriber.on('ap_yd_status').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishYdEngage();
                }
                else {
                    this.publisher.publishYdDisengage();
                }
            });
            this.simVarSubscriber.on('ap_alt_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Alt);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Alt);
                }
            });
            this.simVarSubscriber.on('ap_pitch_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Pitch);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Pitch);
                }
            });
            this.simVarSubscriber.on('ap_heading_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Heading);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Heading);
                }
            });
            this.simVarSubscriber.on('ap_nav_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Nav);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Nav);
                }
            });
            this.simVarSubscriber.on('ap_approach_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Approach);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Approach);
                }
            });
            this.simVarSubscriber.on('ap_backcourse_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Backcourse);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Backcourse);
                }
            });
            this.simVarSubscriber.on('ap_bank_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Bank);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Bank);
                }
            });
            this.simVarSubscriber.on('ap_wing_lvl_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.WingLevel);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.WingLevel);
                }
            });
            this.simVarSubscriber.on('ap_flc_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Flc);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Flc);
                }
            });
            this.simVarSubscriber.on('ap_vs_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Vs);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Vs);
                }
            });
            this.simVarSubscriber.on('ap_glideslope_hold').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.Glideslope);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.Glideslope);
                }
            });
            this.simVarSubscriber.on('vnav_active').whenChangedBy(1).handle((engaged) => {
                if (engaged) {
                    this.publisher.publishLockSet(exports.APLockType.VNav);
                }
                else {
                    this.publisher.publishLockRelease(exports.APLockType.VNav);
                }
            });
        }
        /** update our publishers */
        onUpdate() {
            this.simVarPublisher.onUpdate();
        }
    }

    /**
     * A utility class for working with CDIs.
     */
    class CdiUtils {
        /**
         * Gets the suffix for event bus topics associated with a given CDI index.
         * @param id The index for which to get the suffix.
         * @returns The suffix for event bus topics associated with the specified CDI index.
         */
        static getEventBusTopicSuffix(id) {
            return (id === '' ? '' : `_${id}`);
        }
        /**
         * Checks whether two CDI navigation sources are equal.
         * @param a The first source to check.
         * @param b The second source to check.
         * @returns Whether the two specified CDI navigation sources are equal.
         */
        static navSourceIdEquals(a, b) {
            return a.type === b.type && a.index === b.index;
        }
    }

    /** A collection of helper functions dealing with radios and frequencies. */
    class RadioUtils {
        /**
         * Checks whether a frequency is a NAV frequency.
         * @param freq The frequency to check, in megahertz.
         * @returns Whether the specified frequency is a NAV frequency.
         */
        static isNavFrequency(freq) {
            const freqKhz = Math.round(freq * 1000);
            if (freqKhz < 108e3 || freqKhz > 117950) {
                return false;
            }
            return freqKhz % 50 === 0;
        }
        /**
         * Checks if frequency is a localizer frequency based on the number.
         * @param freq The frequency to check, in megahertz.
         * @returns True if frequency is between 108.1 and 111.95 MHz (inclusive) and the tenths place is odd.
         */
        static isLocalizerFrequency(freq) {
            // Round the frequency to the nearest 10 khz to avoid floating point precision issues.
            const roundedFreq = Math.round(freq * 100);
            return roundedFreq >= 10810 && roundedFreq <= 11195 && (roundedFreq % 20 >= 10);
        }
        /**
         * Checks whether a frequency is a 8.33 kHz-spacing COM frequency.
         * @param freq The frequency to check, in megahertz.
         * @returns Whether the specified frequency is a 8.33 kHz-spacing COM frequency.
         */
        static isCom833Frequency(freq) {
            const freqKhz = Math.round(freq * 1000);
            if (freqKhz < 118e3 || freqKhz > 136990) {
                return false;
            }
            return RadioUtils.COM_833_ENDINGS.includes(freqKhz % 50);
        }
        /**
         * Checks whether a frequency is a 25 kHz-spacing COM frequency.
         * @param freq The frequency to check, in megahertz.
         * @returns Whether the specified frequency is a 25 kHz-spacing COM frequency.
         */
        static isCom25Frequency(freq) {
            const freqKhz = Math.round(freq * 1000);
            if (freqKhz < 118e3 || freqKhz > 136975) {
                return false;
            }
            return freqKhz % 25 === 0;
        }
        /**
         * Checks whether a frequency is an ADF frequency.
         * @param freq The frequency to check, in kilohertz.
         * @returns Whether the specified frequency is an ADF frequency.
         */
        static isAdfFrequency(freq) {
            const freqHz = Math.round(freq * 1000);
            if (freqHz < 190e3 || freqHz > 1799500) {
                return false;
            }
            return freqHz % 500 === 0;
        }
    }
    RadioUtils.COM_833_ENDINGS = [5, 10, 15, 30, 35, 40];

    /**
     * VOR signal to/from flags.
     */
    exports.VorToFrom = void 0;
    (function (VorToFrom) {
        VorToFrom[VorToFrom["OFF"] = 0] = "OFF";
        VorToFrom[VorToFrom["TO"] = 1] = "TO";
        VorToFrom[VorToFrom["FROM"] = 2] = "FROM";
    })(exports.VorToFrom || (exports.VorToFrom = {}));
    /** Marker beacon signal state. */
    exports.MarkerBeaconState = void 0;
    (function (MarkerBeaconState) {
        MarkerBeaconState[MarkerBeaconState["Inactive"] = 0] = "Inactive";
        MarkerBeaconState[MarkerBeaconState["Outer"] = 1] = "Outer";
        MarkerBeaconState[MarkerBeaconState["Middle"] = 2] = "Middle";
        MarkerBeaconState[MarkerBeaconState["Inner"] = 3] = "Inner";
    })(exports.MarkerBeaconState || (exports.MarkerBeaconState = {}));
    /**
     * A publisher of NAV, COM, ADF radio and marker beacon tuning-related sim var events.
     */
    class NavComSimVarPublisher extends SimVarPublisher {
        /**
         * Creates a new instance of NavComSimVarPublisher.
         * @param bus The event bus to which to publish.
         * @param pacer An optional pacer to use to control the pace of publishing
         */
        constructor(bus, pacer = undefined) {
            const simvars = new Map([
                ...NavComSimVarPublisher.createNavRadioDefinitions(),
                ...NavComSimVarPublisher.createComRadioDefinitions(),
                ...NavComSimVarPublisher.createAdfRadioDefinitions(),
                ...NavComSimVarPublisher.createMarkerBeaconDefinitions(),
                ...NavComSimVarPublisher.createGpsDefinitions()
            ]);
            super(simvars, bus, pacer);
        }
        /**
         * Creates an array of nav radio sim var event definitions.
         * @returns An array of nav radio sim var event definitions.
         */
        static createNavRadioDefinitions() {
            return [
                ['nav_active_frequency', { name: 'NAV ACTIVE FREQUENCY:#index#', type: exports.SimVarValueType.MHz, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_standby_frequency', { name: 'NAV STANDBY FREQUENCY:#index#', type: exports.SimVarValueType.MHz, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_signal', { name: 'NAV SIGNAL:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_sound', { name: 'NAV SOUND:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_ident', { name: 'NAV IDENT:#index#', type: exports.SimVarValueType.String, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_volume', { name: 'NAV VOLUME:#index#', type: exports.SimVarValueType.Percent, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_obs', { name: 'NAV OBS:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_has_dme', { name: 'NAV HAS DME:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_has_nav', { name: 'NAV HAS NAV:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_cdi', { name: 'NAV CDI:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_dme', { name: 'NAV DME:#index#', type: exports.SimVarValueType.NM, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_radial', { name: 'NAV RADIAL:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_radial_error', { name: 'NAV RADIAL ERROR:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_to_from', { name: 'NAV TOFROM:#index#', type: exports.SimVarValueType.Enum, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_localizer', { name: 'NAV HAS LOCALIZER:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_localizer_crs', { name: 'NAV LOCALIZER:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_loc_airport_ident', { name: 'NAV LOC AIRPORT IDENT:#index#', type: exports.SimVarValueType.String, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_loc_runway_designator', { name: 'NAV LOC RUNWAY DESIGNATOR:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_loc_runway_number', { name: 'NAV LOC RUNWAY NUMBER:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_glideslope', { name: 'NAV HAS GLIDE SLOPE:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_gs_error', { name: 'NAV GLIDE SLOPE ERROR:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_raw_gs', { name: 'NAV RAW GLIDE SLOPE:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_lla', { name: 'NAV VOR LATLONALT:#index#', type: exports.SimVarValueType.LLA, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_dme_lla', { name: 'NAV DME LATLONALT:#index#', type: exports.SimVarValueType.LLA, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_gs_lla', { name: 'NAV GS LATLONALT:#index#', type: exports.SimVarValueType.LLA, indexed: [1, 2, 3, 4], defaultIndex: null }],
                ['nav_magvar', { name: 'NAV MAGVAR:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2, 3, 4], defaultIndex: null }]
            ];
        }
        /**
         * Creates an array of com radio sim var event definitions.
         * @returns An array of com radio sim var event definitions.
         */
        static createComRadioDefinitions() {
            return [
                ['com_active_frequency', { name: 'COM ACTIVE FREQUENCY:#index#', type: exports.SimVarValueType.MHz, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_standby_frequency', { name: 'COM STANDBY FREQUENCY:#index#', type: exports.SimVarValueType.MHz, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_active_facility_name', { name: 'COM ACTIVE FREQ NAME:#index#', type: exports.SimVarValueType.String, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_active_facility_type', { name: 'COM ACTIVE FREQ TYPE:#index#', type: exports.SimVarValueType.String, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_active_facility_ident', { name: 'COM ACTIVE FREQ IDENT:#index#', type: exports.SimVarValueType.String, indexed: [1, 2, 3], defaultIndex: null }],
                // Note: 'COM RECEIVE' is whether the radio is receiving OR transmitting,
                // whereas 'COM RECEIVE EX1' is exclusively its receiving state.
                ['com_receive', { name: 'COM RECEIVE EX1:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_status', { name: 'COM STATUS:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_transmit', { name: 'COM TRANSMIT:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_spacing_mode', { name: 'COM SPACING MODE:#index#', type: exports.SimVarValueType.Enum, indexed: [1, 2, 3], defaultIndex: null }],
                ['com_volume', { name: 'COM VOLUME:#index#', type: exports.SimVarValueType.Percent, indexed: [1, 2, 3], defaultIndex: null }],
            ];
        }
        /**
         * Creates an array of ADF radio sim var event definitions.
         * @returns An array of ADF radio sim var event definitions.
         */
        static createAdfRadioDefinitions() {
            return [
                ['adf_active_frequency', { name: 'ADF ACTIVE FREQUENCY:#index#', type: exports.SimVarValueType.KHz, indexed: [1, 2], defaultIndex: null }],
                ['adf_standby_frequency', { name: 'ADF STANDBY FREQUENCY:#index#', type: exports.SimVarValueType.KHz, indexed: [1, 2], defaultIndex: null }],
                ['adf_sound', { name: 'ADF SOUND:#index#', type: exports.SimVarValueType.Bool, indexed: [1, 2], defaultIndex: null }],
                ['adf_volume', { name: 'ADF VOLUME:#index#', type: exports.SimVarValueType.Percent, indexed: [1, 2], defaultIndex: null }],
                ['adf_ident', { name: 'ADF IDENT:#index#', type: exports.SimVarValueType.String, indexed: [1, 2], defaultIndex: null }],
                ['adf_signal', { name: 'ADF SIGNAL:#index#', type: exports.SimVarValueType.Number, indexed: [1, 2], defaultIndex: null }],
                ['adf_bearing', { name: 'ADF RADIAL:#index#', type: exports.SimVarValueType.Degree, indexed: [1, 2], defaultIndex: null }],
                ['adf_lla', { name: 'ADF LATLONALT:#index#', type: exports.SimVarValueType.LLA, indexed: [1, 2], defaultIndex: null }]
            ];
        }
        /**
         * Creates an array of GPS sim var event definitions.
         * @returns An array of GPS sim var event definitions.
         */
        static createMarkerBeaconDefinitions() {
            return [
                ['marker_beacon_hisense_on', { name: 'MARKER BEACON SENSITIVITY HIGH', type: exports.SimVarValueType.Bool }],
                ['marker_beacon_sound', { name: 'MARKER SOUND', type: exports.SimVarValueType.Bool }],
                ['marker_beacon_state', { name: 'MARKER BEACON STATE', type: exports.SimVarValueType.Number }],
                ['mkr_bcn_state_simvar', { name: 'MARKER BEACON STATE', type: exports.SimVarValueType.Number }]
            ];
        }
        /**
         * Creates an array of GPS sim var event definitions.
         * @returns An array of GPS sim var event definitions.
         */
        static createGpsDefinitions() {
            return [
                ['gps_dtk', { name: 'GPS WP DESIRED TRACK', type: exports.SimVarValueType.Degree }],
                ['gps_xtk', { name: 'GPS WP CROSS TRK', type: exports.SimVarValueType.NM }],
                ['gps_wp', { name: 'GPS WP NEXT ID', type: exports.SimVarValueType.NM }],
                ['gps_wp_bearing', { name: 'GPS WP BEARING', type: exports.SimVarValueType.Degree }],
                ['gps_wp_distance', { name: 'GPS WP DISTANCE', type: exports.SimVarValueType.NM }],
                ['gps_obs_active_simvar', { name: 'GPS OBS ACTIVE', type: exports.SimVarValueType.Bool }],
                ['gps_obs_value_simvar', { name: 'GPS OBS VALUE', type: exports.SimVarValueType.Degree }]
            ];
        }
    }

    // Common definitions relevant to all radio types.
    /** The basic radio types. */
    exports.RadioType = void 0;
    (function (RadioType) {
        RadioType["Com"] = "COM";
        RadioType["Nav"] = "NAV";
        RadioType["Adf"] = "ADF";
    })(exports.RadioType || (exports.RadioType = {}));
    /** The two frequency "banks", active and standby. */
    exports.FrequencyBank = void 0;
    (function (FrequencyBank) {
        FrequencyBank[FrequencyBank["Active"] = 0] = "Active";
        FrequencyBank[FrequencyBank["Standby"] = 1] = "Standby";
    })(exports.FrequencyBank || (exports.FrequencyBank = {}));
    /** COM frequency spacing on COM radios. */
    exports.ComSpacing = void 0;
    (function (ComSpacing) {
        /** 25Khz spacing */
        ComSpacing[ComSpacing["Spacing25Khz"] = 0] = "Spacing25Khz";
        /** 8.33Khz spacing */
        ComSpacing[ComSpacing["Spacing833Khz"] = 1] = "Spacing833Khz";
    })(exports.ComSpacing || (exports.ComSpacing = {}));

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A publisher of nav radio, ADF radio, GPS, and marker beacon-related sim var events.
     *
     * @deprecated Please use `NavComSimVarPublisher` instead.
     */
    class NavProcSimVarPublisher extends SimVarPublisher {
        /**
         * Create a NavProcSimVarPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the pace of publishing
         */
        constructor(bus, pacer = undefined) {
            super(NavProcSimVarPublisher.simvars, bus, pacer);
        }
        /**
         * Creates an array of nav radio sim var event definitions for an indexed nav radio.
         * @param index The index of the nav radio.
         * @returns An array of nav radio sim var event definitions for the specified nav radio.
         */
        static createNavRadioDefinitions(index) {
            return [
                [`nav_signal_${index}`, { name: `NAV SIGNAL:${index}`, type: exports.SimVarValueType.Number }],
                [`nav_obs_${index}`, { name: `NAV OBS:${index}`, type: exports.SimVarValueType.Degree }],
                [`nav_has_dme_${index}`, { name: `NAV HAS DME:${index}`, type: exports.SimVarValueType.Bool }],
                [`nav_has_nav_${index}`, { name: `NAV HAS NAV:${index}`, type: exports.SimVarValueType.Bool }],
                [`nav_cdi_${index}`, { name: `NAV CDI:${index}`, type: exports.SimVarValueType.Number }],
                [`nav_dme_${index}`, { name: `NAV DME:${index}`, type: exports.SimVarValueType.NM }],
                [`nav_radial_${index}`, { name: `NAV RADIAL:${index}`, type: exports.SimVarValueType.Degree }],
                [`nav_radial_error_${index}`, { name: `NAV RADIAL ERROR:${index}`, type: exports.SimVarValueType.Degree }],
                [`nav_ident_${index}`, { name: `NAV IDENT:${index}`, type: exports.SimVarValueType.String }],
                [`nav_to_from_${index}`, { name: `NAV TOFROM:${index}`, type: exports.SimVarValueType.Enum }],
                [`nav_localizer_${index}`, { name: `NAV HAS LOCALIZER:${index}`, type: exports.SimVarValueType.Bool }],
                [`nav_localizer_crs_${index}`, { name: `NAV LOCALIZER:${index}`, type: exports.SimVarValueType.Number }],
                [`nav_loc_airport_ident_${index}`, { name: `NAV LOC AIRPORT IDENT:${index}`, type: exports.SimVarValueType.String }],
                [`nav_loc_runway_designator_${index}`, { name: `NAV LOC RUNWAY DESIGNATOR:${index}`, type: exports.SimVarValueType.Number }],
                [`nav_loc_runway_number_${index}`, { name: `NAV LOC RUNWAY NUMBER:${index}`, type: exports.SimVarValueType.Number }],
                [`nav_glideslope_${index}`, { name: `NAV HAS GLIDE SLOPE:${index}`, type: exports.SimVarValueType.Bool }],
                [`nav_gs_error_${index}`, { name: `NAV GLIDE SLOPE ERROR:${index}`, type: exports.SimVarValueType.Degree }],
                [`nav_raw_gs_${index}`, { name: `NAV RAW GLIDE SLOPE:${index}`, type: exports.SimVarValueType.Degree }],
                [`nav_lla_${index}`, { name: `NAV VOR LATLONALT:${index}`, type: exports.SimVarValueType.LLA }],
                [`nav_dme_lla_${index}`, { name: `NAV DME LATLONALT:${index}`, type: exports.SimVarValueType.LLA }],
                [`nav_gs_lla_${index}`, { name: `NAV GS LATLONALT:${index}`, type: exports.SimVarValueType.LLA }],
                [`nav_magvar_${index}`, { name: `NAV MAGVAR:${index}`, type: exports.SimVarValueType.Degree }]
            ];
        }
        /**
         * Creates an array of ADF radio sim var event definitions for an indexed ADF radio.
         * @param index The index of the ADF radio.
         * @returns An array of ADF radio sim var event definitions for the specified ADF radio.
         */
        static createAdfRadioDefinitions(index) {
            return [
                [`adf_signal_${index}`, { name: `ADF SIGNAL:${index}`, type: exports.SimVarValueType.Number }],
                [`adf_bearing_${index}`, { name: `ADF RADIAL:${index}`, type: exports.SimVarValueType.Degree }],
                [`adf_lla_${index}`, { name: `ADF LATLONALT:${index}`, type: exports.SimVarValueType.LLA }]
            ];
        }
    }
    NavProcSimVarPublisher.simvars = new Map([
        ...NavProcSimVarPublisher.createNavRadioDefinitions(1),
        ...NavProcSimVarPublisher.createNavRadioDefinitions(2),
        ...NavProcSimVarPublisher.createNavRadioDefinitions(3),
        ...NavProcSimVarPublisher.createNavRadioDefinitions(4),
        ...NavProcSimVarPublisher.createAdfRadioDefinitions(1),
        ...NavProcSimVarPublisher.createAdfRadioDefinitions(2),
        ['gps_dtk', { name: 'GPS WP DESIRED TRACK', type: exports.SimVarValueType.Degree }],
        ['gps_xtk', { name: 'GPS WP CROSS TRK', type: exports.SimVarValueType.NM }],
        ['gps_wp', { name: 'GPS WP NEXT ID', type: exports.SimVarValueType.NM }],
        ['gps_wp_bearing', { name: 'GPS WP BEARING', type: exports.SimVarValueType.Degree }],
        ['gps_wp_distance', { name: 'GPS WP DISTANCE', type: exports.SimVarValueType.NM }],
        ['mkr_bcn_state_simvar', { name: 'MARKER BEACON STATE', type: exports.SimVarValueType.Number }],
        ['gps_obs_active_simvar', { name: 'GPS OBS ACTIVE', type: exports.SimVarValueType.Bool }],
        ['gps_obs_value_simvar', { name: 'GPS OBS VALUE', type: exports.SimVarValueType.Degree }]
    ]);
    //
    // Navigation event configurations
    //
    exports.NavSourceType = void 0;
    (function (NavSourceType) {
        NavSourceType[NavSourceType["Nav"] = 0] = "Nav";
        NavSourceType[NavSourceType["Gps"] = 1] = "Gps";
        NavSourceType[NavSourceType["Adf"] = 2] = "Adf";
    })(exports.NavSourceType || (exports.NavSourceType = {}));
    /**
     * Encapsulation of the logic for a generic nav source.
     */
    class NavSourceBase {
        /**
         * Create a bearing pointer
         * @param id The navsourceid.
         */
        constructor(id) {
            this._ident = null;
            this._bearing = null;
            this._distance = null;
            this._obs = 0;
            this._deviation = null;
            this._toFrom = exports.VorToFrom.OFF;
            this._glideslopeDeviation = null;
            this._glideslopeAngle = null;
            this._localizerCourse = null;
            this._magneticVariation = null;
            this._isLocalizerFrequency = null;
            this.validHandler = undefined;
            this.identHandler = undefined;
            this.brgHandler = undefined;
            this.distHandler = undefined;
            this.obsHandler = undefined;
            this.deviationHandler = undefined;
            this.toFromHandler = undefined;
            this.glideslopeDeviationHandler = undefined;
            this.glideslopeAngleHandler = undefined;
            this.localizerCourseHandler = undefined;
            this.magvarHandler = undefined;
            this.isLocalizerFrequencyHandler = undefined;
            this._valid = false;
            this._activeBrg = false;
            this._activeCdi = false;
            this._hasCdi = false;
            this._hasDme = false;
            this._hasLocalizer = false;
            this._hasGlideslope = false;
            this._signal = 0;
            this._activeForCount = 0;
            if (id.type !== null && id.type in [exports.NavSourceType.Nav, exports.NavSourceType.Gps]) {
                this._hasCdi = true;
            }
            this.valid = false;
            this.srcId = id;
        }
        /**
         * Do we support CDI?
         * @returns A boolean with our CDI support state.
         */
        get hasCdi() {
            return this._hasCdi;
        }
        /**
         * Set a new ident.
         * @param ident The new ident string.
         */
        set ident(ident) {
            this._ident = ident;
            if (this.valid && this.activeBrg && this.identHandler !== undefined) {
                this.identHandler(ident, this.srcId);
            }
        }
        /**
         * Get an ident.
         * @returns A string identifying the nav source.
         */
        get ident() {
            if (this._signal > 0) {
                return this._ident;
            }
            else {
                return null;
            }
        }
        /**
         * Set a new bearing.
         * @param bearing The new bearing in degrees.
         */
        set bearing(bearing) {
            if (bearing !== null) {
                bearing = (bearing + 180) % 360;
            }
            this._bearing = bearing;
            if (this.valid && this.activeBrg && this.brgHandler !== undefined) {
                this.brgHandler(bearing, this.srcId);
            }
        }
        /**
         * Get abearing.
         * @returns Bearing to the source in degrees.
         */
        get bearing() {
            return this._bearing;
        }
        /**
         * Set a new distance
         * @param distance The distance in NM.
         */
        set distance(distance) {
            this._distance = distance;
            if (this.valid && this.activeBrg && this.distHandler !== undefined) {
                this.distHandler(this.distance, this.srcId);
            }
        }
        /**
         * Get the distance to a source..
         * @returns Distance to the source in degrees.
         */
        get distance() {
            if (this.hasDme) {
                return this._distance;
            }
            else {
                return null;
            }
        }
        /**
         * Set a new OBS
         * @param obs the new bearing in degrees
         */
        set obs(obs) {
            this._obs = obs;
            if (this.activeCdi && this.obsHandler !== undefined) {
                this.obsHandler(obs, this.srcId);
            }
        }
        /**
         * Get the OBS setting.
         * @returns OBS in degrees.
         */
        get obs() {
            return this._obs;
        }
        /**
         * Set a new deviation
         * @param deviation The new deviation in points.
         */
        set deviation(deviation) {
            this._deviation = deviation;
            if (this.activeCdi && this.deviationHandler !== undefined) {
                this.deviationHandler(deviation, this.srcId);
            }
        }
        /**
         * Get the deviation.
         * @returns The deviation in points.
         */
        get deviation() {
            return this._deviation;
        }
        /**
         * Set a new VOR to/from value
         * @param toFrom The to/from value.
         */
        set toFrom(toFrom) {
            if (this.activeCdi && this.toFromHandler !== undefined) {
                this.toFromHandler(toFrom, this.srcId);
            }
            this._toFrom = toFrom;
        }
        /**
         * Get the VOR to/from value.
         * @returns The VOR to/from value.
         */
        get toFrom() {
            return this._toFrom;
        }
        /**
         * Set whether the nav radio has a loc signal
         * @param valid If the loc exists.
         */
        set hasLocalizer(valid) {
            this._hasLocalizer = valid;
            if (!this._hasLocalizer) {
                this.localizerCourse = -1;
            }
            else if (this._hasLocalizer && this.localizerCourseHandler && this.localizerCourseHandler !== null
                && this._localizerCourse !== null && this._localizerCourse >= 0) {
                this.localizerCourseHandler(this._localizerCourse, this.srcId);
            }
        }
        /**
         * Get if the radio has a loc.
         * @returns The loc bool.
         */
        get hasLocalizer() {
            return this._hasLocalizer;
        }
        /**
         * Set a new localizerCourse
         * @param course The new localizer course.
         */
        set localizerCourse(course) {
            this._localizerCourse = course !== null ? course * (180 / Math.PI) : -1;
            if (this._localizerCourse !== -1 && this.localizerCourseHandler !== undefined) {
                this.localizerCourseHandler(this._localizerCourse, this.srcId);
            }
        }
        /**
         * Get the localizerCourse.
         * @returns The glideslopeDeviation in degrees.
         */
        get localizerCourse() {
            return this._localizerCourse;
        }
        /**
         * Set whether the nav radio has a GS signal
         * @param valid If the GS exists.
         */
        set hasGlideslope(valid) {
            this._hasGlideslope = valid;
            if (this.glideslopeDeviationHandler !== undefined) {
                this.glideslopeDeviationHandler(this.glideslopeDeviation, this.srcId);
            }
        }
        /**
         * Get if the radio has a glideslope.
         * @returns The glideslope bool.
         */
        get hasGlideslope() {
            return this._hasGlideslope;
        }
        /**
         * Set a new _glideslopeDeviation
         * @param deviation The new deviation in points.
         */
        set glideslopeDeviation(deviation) {
            this._glideslopeDeviation = deviation;
            if (this.valid && this.activeCdi && this.hasGlideslope && this.glideslopeDeviationHandler !== undefined) {
                this.glideslopeDeviationHandler(deviation, this.srcId);
            }
        }
        /**
         * Get the glideslopeDeviation.
         * @returns The glideslopeDeviation in degrees.
         */
        get glideslopeDeviation() {
            return this._glideslopeDeviation;
        }
        /**
         * Set a new _glideslopeAngle
         * @param angle The new angle in degrees.
         */
        set glideslopeAngle(angle) {
            this._glideslopeAngle = angle;
            if (this.valid && this.activeCdi && this.hasGlideslope && this.glideslopeAngleHandler !== undefined) {
                this.glideslopeAngleHandler(angle, this.srcId);
            }
        }
        /**
         * Get the glideslopeAngle.
         * @returns The glideslopeAngle in degrees.
         */
        get glideslopeAngle() {
            return this._glideslopeAngle;
        }
        /**
         * Set whether there's a valid DME signal.
         * @param hasDme Whether a nav signal is present or not.
         */
        set hasDme(hasDme) {
            this._hasDme = hasDme;
            if (this.distHandler !== undefined) {
                // If hasDme state is changing, we need to publish a new distance event.
                // The getter for this.distance handles whether there's a DME signal or not.
                this.distHandler(this.distance, this.srcId);
            }
        }
        /**
         * Get whether there's a valid DME signal.
         * @returns A boolean indicatind presence of DME.
         */
        get hasDme() {
            return this._hasDme;
        }
        /**
         * Get the nav radio magvar.
         * @returns The magvar value.
         */
        get magneticVariation() {
            return this._magneticVariation;
        }
        /**
         * Set a new nav radio magvar
         * @param magvar The new nav radio magvar.
         */
        set magneticVariation(magvar) {
            this._magneticVariation = magvar;
            if (this.valid && this.activeCdi && this.magvarHandler !== undefined) {
                this.magvarHandler(magvar, this.srcId);
            }
        }
        /**
         * Get if the frequency is for a localizer.
         * @returns a bool of whether the freq is for a localizer.
         */
        get isLocalizerFrequency() {
            return this._isLocalizerFrequency;
        }
        /**
         * Sets if a frequency is for a localizer
         * @param isLocFreq whether the freq is a localizer freq.
         */
        set isLocalizerFrequency(isLocFreq) {
            if (this.isLocalizerFrequencyHandler !== undefined) {
                this.isLocalizerFrequencyHandler(isLocFreq, this.srcId);
            }
            this._isLocalizerFrequency = isLocFreq;
        }
        /**
         * Set the nav signal strength.
         * @param signal The signal strength as a number.
         */
        set signal(signal) {
            if ((this._signal == 0) != (signal == 0)) {
                // if we gain or lose signal, we need to update our ident.
                this._signal = signal;
                if (this.identHandler !== undefined) {
                    // The getter for ident will handle whether we have signal or not.
                    this.identHandler(this.ident, this.srcId);
                }
            }
            else {
                // repaet this from above to avoid a needless temporary assigment.
                this._signal = signal;
            }
        }
        /**
         * Set validity.
         * @param valid Whether we are valid or not.
         */
        set valid(valid) {
            // TODO Make sure this matches up with new activeBrg logic
            this._valid = valid;
            this.validHandler && this.validHandler(valid, this.srcId);
            if (valid && this.activeBrg) {
                if (this.identHandler !== undefined) {
                    this.identHandler(this._ident, this.srcId);
                }
                if (this.brgHandler !== undefined) {
                    this.brgHandler(this._bearing, this.srcId);
                }
                if (this.distHandler !== undefined) {
                    this.distHandler(this._distance, this.srcId);
                }
                if (this.toFromHandler !== undefined) {
                    this.toFromHandler(this._toFrom, this.srcId);
                }
                if (this.localizerCourseHandler !== undefined) {
                    this.localizerCourseHandler(this._localizerCourse, this.srcId);
                }
                if (this.glideslopeDeviationHandler !== undefined) {
                    this.glideslopeDeviationHandler(this._glideslopeDeviation, this.srcId);
                }
                if (this.magvarHandler !== undefined) {
                    this.magvarHandler(this._magneticVariation, this.srcId);
                }
                if (this.isLocalizerFrequencyHandler !== undefined) {
                    this.isLocalizerFrequencyHandler(this._isLocalizerFrequency, this.srcId);
                }
                if (this.obsHandler !== undefined) {
                    this.obsHandler(this._obs, this.srcId);
                }
            }
            else if (this.activeBrg) {
                if (this.identHandler !== undefined) {
                    this.identHandler(null, this.srcId);
                }
                if (this.brgHandler !== undefined) {
                    this.brgHandler(null, this.srcId);
                }
                if (this.distHandler !== undefined) {
                    this.distHandler(null, this.srcId);
                }
                if (this.toFromHandler !== undefined) {
                    this.toFromHandler(this._toFrom, this.srcId);
                }
                if (this.localizerCourseHandler !== undefined) {
                    this.localizerCourseHandler(null, this.srcId);
                }
                if (this.glideslopeDeviationHandler !== undefined) {
                    this.glideslopeDeviationHandler(null, this.srcId);
                }
                if (this.magvarHandler !== undefined) {
                    this.magvarHandler(null, this.srcId);
                }
                if (this.isLocalizerFrequencyHandler !== undefined) {
                    this.isLocalizerFrequencyHandler(this._isLocalizerFrequency, this.srcId);
                }
                if (this.obsHandler !== undefined) {
                    this.obsHandler(this._obs, this.srcId);
                }
            }
        }
        /**
         * Get validity
         * @returns A boolean indicating whether this is valid
         */
        get valid() {
            return this._valid;
        }
        /**
         * Set as active for bearing information
         * @param active Whether we are active for bearing info.
         */
        set activeBrg(active) {
            if (active) {
                this._activeForCount++;
            }
            else if (this._activeForCount > 0) {
                this._activeForCount--;
            }
            this.validHandler && this.validHandler(this.valid, this.srcId);
            if (!this.activeBrg || !this.valid) {
                if (this.identHandler !== undefined) {
                    this.identHandler(null, this.srcId);
                }
                if (this.brgHandler !== undefined) {
                    this.brgHandler(null, this.srcId);
                }
                if (this.distHandler !== undefined) {
                    this.distHandler(null, this.srcId);
                }
                if (this.toFromHandler !== undefined) {
                    this.toFromHandler(this._toFrom, this.srcId);
                }
                if (this.isLocalizerFrequencyHandler !== undefined) {
                    this.isLocalizerFrequencyHandler(this._isLocalizerFrequency, this.srcId);
                }
            }
            else {
                if (this.identHandler !== undefined) {
                    this.identHandler(this.ident, this.srcId);
                }
                if (this.brgHandler !== undefined) {
                    this.brgHandler(this.bearing, this.srcId);
                }
                if (this.distHandler !== undefined) {
                    this.distHandler(this.distance, this.srcId);
                }
                if (this.toFromHandler !== undefined) {
                    this.toFromHandler(this._toFrom, this.srcId);
                }
                if (this.isLocalizerFrequencyHandler !== undefined) {
                    this.isLocalizerFrequencyHandler(this._isLocalizerFrequency, this.srcId);
                }
            }
        }
        /**
         * Are we active for bearing information?
         * @returns Our active state
         */
        get activeBrg() {
            return this._activeForCount > 0;
        }
        /**
         * Set this at the active deviation source or not
         * @param active Whether we are active for publishing deviation info.
         */
        set activeCdi(active) {
            this._activeCdi = active;
            if (active && this.deviationHandler !== undefined) {
                this.deviationHandler(this._deviation, this.srcId);
            }
        }
        /**
         * Are we active for CDI data?
         * @returns Boolean of our CDI active state
         */
        get activeCdi() {
            return this._activeCdi;
        }
    }
    /**
     * A convenience class for creating a navproc configuration set.
     *
     * Implementers should instantiate this and then populate the sets with the
     * HEvents that their radio sends for various actions.
     */
    class NavProcessorConfig {
        constructor() {
            this.numNav = 2;
            this.numGps = 1;
            this.numAdf = 1;
            this.courseIncEvents = new Set();
            this.courseDecEvents = new Set();
            this.courseSyncEvents = new Set();
            this.additionalSources = new Array();
        }
    }
    /**
     * A publisher for navigation processor events.
     */
    class NavProcPublisher extends BasePublisher {
        /**
         * Creates a NavProcPublisher
         * @param bus The event bus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer) {
            super(bus, pacer);
        }
        /**
         * Publish a new CDI source selection.
         * @param source The selected NavSource.
         */
        publishCdiSelect(source) {
            this.publish('cdi_select', source, true);
        }
        /**
         * Publish a new CDI deviation
         * @param deviation The deviation
         * @param source the source for thes deviation
         */
        publishDeviation(deviation, source) {
            this.publish('cdi_deviation', { source: source, deviation: deviation });
        }
        /**
         * Publish a new OBS heading
         * @param heading The heading
         * @param source The source for this heading.
         */
        publishObsHeading(heading, source) {
            this.publish('obs_set', { source: source, heading: heading });
        }
        /**
         * Publish new validity information.
         * @param index The bearing index number to update.
         * @param valid The validity state of that berign source.
         */
        publishBrgValidity(index, valid) {
            this.publish('brg_validity', { index: index, valid: valid }, true);
        }
        /**
         * Publish a new bearing source
         * @param index The source number.
         * @param source The source name.
         */
        publishBrgSrc(index, source) {
            this.publish('brg_source', { index: index, source: source });
        }
        /**
         * Publish heading of a bearing source.
         * @param index The index number to update.
         * @param direction The direction.
         */
        publishBrgDir(index, direction) {
            this.publish('brg_direction', { index: index, direction: direction });
        }
        /**
         * Publish distance to a bearing source.
         * @param index The source number.
         * @param distance The distance in NMs.
         */
        publishBrgDist(index, distance) {
            this.publish('brg_distance', { index: index, distance: distance });
        }
        /**
         * Publish distance to a bearing source.
         * @param index The source number.
         * @param ident The ident as a string.
         * @param isLoc is this source a loc.
         */
        publishBrgIdent(index, ident, isLoc) {
            this.publish('brg_ident', { index: index, ident: ident, isLoc: isLoc });
        }
        // /**
        //  * Publish distance to a bearing source.
        //  * @param index The source number.
        //  * @param isLoc The ident as a string.
        //  */
        // public publishBrgIsLoc(index: number, isLoc: boolean | null): void {
        //     this.publish('brg_is_loc', { index: index, isLoc: isLoc });
        // }
        /**
         * Publish to/from value for a nav source.
         * @param toFrom The to/from enum value.
         * @param source The nav radio source.
         */
        publishToFrom(toFrom, source) {
            this.publish('vor_to_from', { toFrom: toFrom, source: source });
        }
        /**
         * Publish localizer value for a nav source.
         * @param localizer is the localizer data
         * @param source The nav radio source.
         */
        publishLocalizer(localizer, source) {
            this.publish('localizer', { isValid: localizer.isValid, course: localizer.course, source: source });
        }
        /**
         * Publish if the nav source is tuned to a localizer frequency.
         * @param isLoc is a bool whether or not the nav source is a loc frequency
         * @param source The nav radio source.
         */
        publishIsLocalizerFrequency(isLoc, source) {
            this.publish('is_localizer_frequency', { isLocalizer: isLoc.isLocalizer, source: source });
        }
        /**
         * Publish gliseslope value for a nav source.
         * @param glideslope is the localizer data
         * @param source The nav radio source.
         */
        publishGlideslope(glideslope, source) {
            this.publish('glideslope', { isValid: glideslope.isValid, deviation: glideslope.deviation, gsAngle: glideslope.gsAngle, source: source });
        }
        /**
         * Publish magvar value for a nav source.
         * @param variation is the magnetic variation
         * @param source The nav radio source.
         */
        publishMagvar(variation, source) {
            if (variation !== null) {
                this.publish('mag_variation', { variation: variation, source: source });
            }
        }
        /**
         * Publish marker beacon state value.
         * @param state is the marker beacon state value
         */
        publishMarkerBeacon(state) {
            this.publish('mkr_bcn_state', state);
        }
        /**
         * Publish DME state.
         * @param state is the has_dme state value
         * @param distance is the dme distance value.
         * @param source The nav radio source.
         */
        publishDmeState(state, distance, source) {
            this.publish('dme_state', { hasDme: state, dmeDistance: distance, source: source });
        }
        /**
         * Publish GPS OBS State.
         * @param state is the GPS OBS Active State
         */
        publishGpsObsState(state) {
            this.publish('gps_obs_active', state, true);
        }
        /**
         * Publish GPS OBS Value.
         * @param value is the GPS OBS heading value
         */
        publishGpsObsValue(value) {
            this.publish('gps_obs_value', value, true);
        }
    }
    /**
     * The core of tne nav processor
     */
    class NavProcessor {
        /**
         * Create a NavProcessor.
         * @param bus The event bus to publish to.
         * @param config A config object defining our radio options.
         */
        constructor(bus, config) {
            this.brgSrcAsoboMap = [-1, 0, 1, 3, 2];
            /**
             * Handle HEvents
             * @param event The hEvent name
             */
            this.eventHandler = (event) => {
                if (this.config.courseIncEvents.has(event)) {
                    this.handleCrsInc();
                }
                else if (this.config.courseDecEvents.has(event)) {
                    this.handleCrsDec();
                }
                else if (this.config.courseSyncEvents.has(event)) {
                    this.handleCrsSync();
                }
            };
            /**
             * Toggles CDI between GPS and NAV1.
             */
            this.onCdiGpsToggle = () => {
                const src = this.navSources[this.cdiSourceIdx];
                if (src.srcId.type === exports.NavSourceType.Gps) {
                    this.switchCdiSrc();
                }
                else {
                    this.switchCdiSrc(3);
                }
            };
            this.bus = bus;
            this.config = config;
            this.publisher = new NavProcPublisher(bus);
            this.hEvents = bus.getSubscriber();
            this.controlSubscriber = bus.getSubscriber();
            this.navComSubscriber = bus.getSubscriber();
            this.radioEventsSubscriber = bus.getSubscriber();
            this.navSources = new Array();
            this.bearingSourceIdxs = [-1, -1];
            this.cdiSourceIdx = 0;
        }
        /**
         * Initialize a nav processor
         */
        init() {
            this.publisher.startPublish();
            this.hEvents.on('hEvent').handle(this.eventHandler);
            this.controlSubscriber.on('cdi_src_switch').handle(() => {
                this.switchCdiSrc();
            });
            this.controlSubscriber.on('cdi_src_set').handle((src) => {
                if (src.type === exports.NavSourceType.Gps) {
                    this.switchCdiSrc(3);
                }
                else if (src.type === exports.NavSourceType.Nav) {
                    this.switchCdiSrc(src.index - 1);
                }
            });
            this.controlSubscriber.on('cdi_src_gps_toggle').handle(this.onCdiGpsToggle);
            this.controlSubscriber.on('init_cdi').handle(this.initCdi.bind(this));
            this.controlSubscriber.on('brg_src_switch').handle(this.cycleBrgSrc.bind(this));
            // TODO Determine why this throttle doesn't work but does on the client end.
            this.navComSubscriber.on('mkr_bcn_state_simvar').whenChanged().handle((state) => {
                this.publisher.publishMarkerBeacon(state);
            });
            this.navComSubscriber.on('gps_obs_active_simvar').whenChanged().handle((state) => {
                this.publisher.publishGpsObsState(state);
            });
            this.navComSubscriber.on('gps_obs_value_simvar').whenChangedBy(1).handle((value) => {
                this.publisher.publishGpsObsValue(value);
            });
            for (let i = 1; i <= this.config.numNav; i++) {
                const index = i;
                const src = new NavSourceBase({ type: exports.NavSourceType.Nav, index });
                src.deviationHandler = this.publisher.publishDeviation.bind(this.publisher);
                src.obsHandler = this.publisher.publishObsHeading.bind(this.publisher);
                src.distHandler = this.onBrgDistance.bind(this);
                src.brgHandler = this.onBrgDirection.bind(this);
                src.identHandler = this.onBrgIdent.bind(this);
                src.toFromHandler = this.publisher.publishToFrom.bind(this.publisher);
                src.localizerCourseHandler = this.onLocalizerCourse.bind(this);
                src.glideslopeDeviationHandler = this.onGlideslopeDeviation.bind(this);
                src.magvarHandler = this.publisher.publishMagvar.bind(this.publisher);
                src.isLocalizerFrequencyHandler = this.onIsLocalizerFrequency.bind(this);
                src.glideslopeAngleHandler = this.onGlideslopeAngle.bind(this);
                src.validHandler = this.onBrgValidity.bind(this);
                this.navComSubscriber.on(`nav_cdi_${index}`).whenChangedBy(1).handle((deviation) => {
                    src.deviation = deviation;
                });
                this.navComSubscriber.on(`nav_obs_${index}`).whenChangedBy(1).handle((obs) => {
                    src.obs = obs;
                });
                this.navComSubscriber.on(`nav_dme_${index}`).whenChangedBy(0.1).handle((distance) => {
                    src.distance = distance;
                    // TODO Fold DME logic into the distance handler.
                    this.onDme(src.hasDme, src.distance, src.srcId);
                });
                this.navComSubscriber.on(`nav_radial_${index}`).handle((bearing) => {
                    src.bearing = bearing;
                });
                this.navComSubscriber.on(`nav_ident_${index}`).whenChanged().handle((ident) => {
                    src.ident = ident;
                });
                this.navComSubscriber.on(`nav_signal_${index}`).withPrecision(0).handle((signal) => {
                    src.signal = signal;
                });
                this.navComSubscriber.on(`nav_has_nav_${index}`).whenChanged().handle((valid) => {
                    src.valid = !!valid;
                });
                this.navComSubscriber.on(`nav_has_dme_${index}`).whenChanged().handle((dme) => {
                    src.hasDme = !!dme;
                    // TODO Fold DME logic into the distance handler.
                    this.onDme(src.hasDme, src.distance, src.srcId);
                });
                this.navComSubscriber.on(`nav_to_from_${index}`).whenChanged().handle((value) => {
                    src.toFrom = value;
                });
                this.navComSubscriber.on(`nav_localizer_${index}`).whenChanged().handle((localizer) => {
                    src.hasLocalizer = localizer;
                });
                this.navComSubscriber.on(`nav_localizer_crs_${index}`).whenChanged().handle((locCourse) => {
                    src.localizerCourse = locCourse;
                });
                this.navComSubscriber.on(`nav_glideslope_${index}`).whenChanged().handle((gs) => {
                    src.hasGlideslope = gs;
                });
                this.navComSubscriber.on(`nav_gs_error_${index}`).whenChanged().handle((gsDev) => {
                    src.glideslopeDeviation = gsDev;
                });
                this.navComSubscriber.on(`nav_raw_gs_${index}`).whenChanged().handle((rawGs) => {
                    src.glideslopeAngle = rawGs;
                });
                this.navComSubscriber.on(`nav_magvar_${index}`).whenChanged().handle((magvar) => {
                    src.magneticVariation = magvar;
                });
                this.radioEventsSubscriber.on('set_radio_state').handle((radioState) => {
                    if (radioState.radioType === exports.RadioType.Nav && radioState.index == i && radioState.activeFrequency) {
                        src.isLocalizerFrequency = this.frequencyIsLocalizer(radioState.activeFrequency);
                    }
                });
                this.radioEventsSubscriber.on('set_frequency').handle((setFrequency) => {
                    if (setFrequency.radio.radioType === exports.RadioType.Nav && setFrequency.radio.index == i
                        && setFrequency.bank == exports.FrequencyBank.Active) {
                        src.isLocalizerFrequency = this.frequencyIsLocalizer(setFrequency.frequency);
                        for (let j = 0; j < this.bearingSourceIdxs.length; j++) {
                            if (this.navSources[this.bearingSourceIdxs[j]] !== undefined) {
                                const source = this.navSources[this.bearingSourceIdxs[j]].srcId;
                                if (source.type === exports.NavSourceType.Nav && source.index === i) {
                                    this.setBrgSrc(j, this.bearingSourceIdxs[j]);
                                }
                            }
                        }
                    }
                });
                this.navSources.push(src);
            }
            // TODO Add support for multiple GPSes
            if (this.config.numGps > 0) {
                // Set the CDI source to the first GPS (which, since we're zero-indexed, is the
                // same as the number of nav radios.
                this.cdiSourceIdx = this.config.numNav;
                const src = new NavSourceBase({ type: exports.NavSourceType.Gps, index: 1 });
                src.valid = true;
                src.deviationHandler = this.publisher.publishDeviation.bind(this.publisher);
                src.obsHandler = this.publisher.publishObsHeading.bind(this.publisher);
                src.distHandler = this.onBrgDistance.bind(this);
                src.brgHandler = this.onBrgDirection.bind(this);
                src.identHandler = this.onBrgIdent.bind(this);
                this.navComSubscriber.on('gps_xtk').whenChangedBy(1).handle((deviation) => {
                    src.deviation = deviation;
                });
                this.navComSubscriber.on('gps_dtk').whenChangedBy(1).handle((obs) => {
                    src.obs = obs;
                });
                this.navComSubscriber.on('gps_wp_bearing').withPrecision(2).handle((brg) => {
                    // The nav source bearing expects to be a radial, not the bearing to the
                    // waypoint.  When we get the bearing from the GPS, we need to invert it
                    // so the NavSource knows how to handle it correctly.
                    // TODO Make bearing handling in NavSourceBase more consistent.
                    brg = (brg * Avionics.Utils.RAD2DEG + 180) % 360;
                    src.bearing = brg;
                });
                this.navSources.push(src);
            }
            if (this.config.numAdf > 0) {
                const src = new NavSourceBase({ type: exports.NavSourceType.Adf, index: 1 });
                src.valid = false;
                src.signal = 0;
                src.brgHandler = this.onBrgDirection.bind(this);
                src.identHandler = this.onBrgIdent.bind(this);
                src.validHandler = this.onBrgValidity.bind(this);
                this.navComSubscriber.on('adf_signal_1').withPrecision(0).handle((signal) => {
                    src.signal = signal;
                    if (signal > 0) {
                        if (!src.valid) {
                            src.valid = true;
                        }
                    }
                    else {
                        if (src.valid) {
                            src.valid = false;
                        }
                    }
                });
                this.radioEventsSubscriber.on('adf_active_frequency_1').handle((freq) => {
                    if (src.ident !== freq.toFixed(1)) {
                        src.ident = freq.toFixed(1);
                    }
                });
                this.navComSubscriber.on('adf_bearing_1').withPrecision(2).handle((brg) => {
                    brg = (brg + 180) % 360;
                    src.bearing = brg;
                });
                this.navSources.push(src);
            }
            for (const source of this.config.additionalSources) {
                this.addNavSource(source);
            }
            // HINT: Initialize cdi source based on FLT
            const initGpsDrivesNav1 = SimVar.GetSimVarValue('GPS DRIVES NAV1', exports.SimVarValueType.Bool);
            this.cdiSourceIdx = initGpsDrivesNav1 ? this.getFirstNavSourceIndexByType(exports.NavSourceType.Gps) : this.getFirstNavSourceIndexByType(exports.NavSourceType.Nav);
            // HINT: Initialize bearing sources based on FLT (compatability with current missions)
            for (let i = 0; i < 2; i++) {
                const fltBrgSrc = SimVar.GetSimVarValue(`L:PFD_BRG${i + 1}_Source`, exports.SimVarValueType.Number);
                this.setBrgSrc(i, this.brgSrcAsoboMap[fltBrgSrc]);
            }
        }
        /**
         * Add a custom nav source to the processor.
         * @param source The implementation of NavSourceBase to add.
         */
        addNavSource(source) {
            // TODO Add remaining handlers here for other custom nav sources.
            if (source.validHandler) {
                source.validHandler = this.onBrgValidity.bind(this);
            }
            if (source.brgHandler) {
                source.brgHandler = this.onBrgDirection.bind(this);
            }
            if (source.distHandler) {
                source.distHandler = this.onBrgDistance.bind(this);
            }
            if (source.identHandler) {
                source.identHandler = this.onBrgIdent.bind(this);
            }
            this.navSources.push(source);
        }
        /**
         * Process a CDI source change event.
         * @param index is specified if a specific cdiSourceIdx is requested
         */
        switchCdiSrc(index) {
            let src = this.navSources[this.cdiSourceIdx];
            src.activeCdi = false;
            if (index !== undefined && index < this.navSources.length && this.navSources[index].hasCdi) {
                //PM Modif don't change if identical
                if(this.cdiSourceIdx == index)
                    return;
                //End PM Modif
                this.cdiSourceIdx = index;
            }
            else if (index === undefined) {
                do {
                    this.cdiSourceIdx = this.cdiSourceIdx < this.navSources.length - 1 ? this.cdiSourceIdx + 1 : 0;
                } while (!this.navSources[this.cdiSourceIdx].hasCdi);
            }
            else {
                console.warn(`switchCdiSrc: Unable to set CDI Source index ${index}`);
                return;
            }
            src = this.navSources[this.cdiSourceIdx];
            src.activeCdi = true;
            this.publisher.publishCdiSelect(src.srcId);
            if (src.obs) {
                this.publisher.publishObsHeading(src.obs, src.srcId);
            }
            if (src.toFrom) {
                this.publisher.publishToFrom(src.toFrom, src.srcId);
            }
            this.publisher.publishDmeState(src.hasDme, src.distance, src.srcId);
            SimVar.SetSimVarValue('GPS DRIVES NAV1', exports.SimVarValueType.Bool, src.srcId.type === exports.NavSourceType.Gps);
            if (src.srcId.type === exports.NavSourceType.Nav) {
                SimVar.SetSimVarValue('AUTOPILOT NAV SELECTED', exports.SimVarValueType.Number, src.srcId.index);
            }
        }
        /**
         * Initialize the CDI.
         * @param retryIfFailed Whether to retry if initialization fails.
         */
        initCdi(retryIfFailed) {
            const src = this.navSources[this.cdiSourceIdx];
            if (src === undefined) {
                if (retryIfFailed) {
                    setTimeout(() => this.initCdi(false), 1000);
                    console.warn('initCdi: Unable to initialize CDI (NAV source not found). Retrying...');
                }
                else {
                    console.error('initCdi: Unable to initialize CDI (NAV source not found)');
                }
                return;
            }
            src.activeCdi = true;
            this.publisher.publishCdiSelect(src.srcId);
            if (src.obs) {
                this.publisher.publishObsHeading(src.obs, src.srcId);
            }
            if (src.toFrom) {
                this.publisher.publishToFrom(src.toFrom, src.srcId);
            }
            this.publisher.publishDmeState(src.hasDme, src.distance, src.srcId);
            SimVar.SetSimVarValue('GPS DRIVES NAV1', exports.SimVarValueType.Bool, src.srcId.type === exports.NavSourceType.Gps);
        }
        /**
         * Process a bearing source change event.
         * @param index The index of the source to change (1-based).
         */
        cycleBrgSrc(index) {
            index--;
            let newNavSrcIndex = -1;
            if (this.bearingSourceIdxs[index] < this.navSources.length - 1) {
                newNavSrcIndex = this.bearingSourceIdxs[index] + 1;
            }
            this.setBrgSrc(index, newNavSrcIndex);
        }
        /**
         * Set the bearing source to the specified nav source index.
         * @param bearingSrcIndex The index of the bearing source to change (0-based).
         * @param navSrcIndex The index of the nav source to change to (0-based).
         */
        setBrgSrc(bearingSrcIndex, navSrcIndex) {
            if (bearingSrcIndex > this.bearingSourceIdxs.length - 1
                || navSrcIndex > this.navSources.length - 1) {
                console.warn(`setBrgSrc: Unable to set bearing source index ${bearingSrcIndex} and nav source index ${navSrcIndex}`);
                return;
            }
            const oldSrc = this.navSources[this.bearingSourceIdxs[bearingSrcIndex]];
            if (oldSrc !== undefined) {
                oldSrc.activeBrg = false;
            }
            this.bearingSourceIdxs[bearingSrcIndex] = navSrcIndex;
            const newSrc = this.navSources[this.bearingSourceIdxs[bearingSrcIndex]];
            this.publisher.publishBrgSrc(bearingSrcIndex, newSrc !== undefined ? newSrc.srcId : null);
            // HINT setting brg source LVar for mission compatability
            SimVar.SetSimVarValue(`L:PFD_BRG${bearingSrcIndex + 1}_Source`, exports.SimVarValueType.Number, this.brgSrcAsoboMap.indexOf(navSrcIndex));
            if (newSrc !== undefined) {
                newSrc.activeBrg = true;
            }
            newSrc && this.publisher.publishBrgValidity(bearingSrcIndex, newSrc.valid);
            if (newSrc === undefined) {
                this.publisher.publishBrgIdent(bearingSrcIndex, null, false);
                this.publisher.publishBrgDir(bearingSrcIndex, null);
                this.publisher.publishBrgDist(bearingSrcIndex, null);
                //this.publisher.publishBrgIsLoc(index, false);
            }
        }
        /**
         * Handle a course inc event if we have a nav radio as our active CDI source.
         */
        handleCrsInc() {
            if (this.navSources[this.cdiSourceIdx].srcId.type != exports.NavSourceType.Nav) {
                return;
            }
            switch (this.navSources[this.cdiSourceIdx].srcId.index) {
                case 1:
                    SimVar.SetSimVarValue('K:VOR1_OBI_INC', 'number', 0);
                    break;
                case 2:
                    SimVar.SetSimVarValue('K:VOR2_OBI_INC', 'number', 0);
            }
        }
        /**
         * Handle a course dec event if we have a nav radio as our active CDI source.
         */
        handleCrsDec() {
            if (this.navSources[this.cdiSourceIdx].srcId.type != exports.NavSourceType.Nav) {
                return;
            }
            switch (this.navSources[this.cdiSourceIdx].srcId.index) {
                case 1:
                    SimVar.SetSimVarValue('K:VOR1_OBI_DEC', 'number', 0);
                    break;
                case 2:
                    SimVar.SetSimVarValue('K:VOR2_OBI_DEC', 'number', 0);
                    break;
            }
        }
        /**
         * Handle a course sync event if we have a nav radio as our active CDI source.
         */
        handleCrsSync() {
            if (this.navSources[this.cdiSourceIdx].srcId.type != exports.NavSourceType.Nav) {
                return;
            }
            switch (this.navSources[this.cdiSourceIdx].srcId.index) {
                case 1:
                    SimVar.SetSimVarValue('K:VOR1_SET', 'number', this.navSources[0].bearing);
                    break;
                case 2:
                    SimVar.SetSimVarValue('K:VOR2_SET', 'number', this.navSources[1].bearing);
                    break;
            }
        }
        /**
         * Handle a bearing validity change.
         * @param valid The new bearing validity
         * @param source The source of
         */
        onBrgValidity(valid, source) {
            if (this.bearingSourceIdxs) {
                for (let i = 0; i < this.bearingSourceIdxs.length; i++) {
                    if (this.navSources[this.bearingSourceIdxs[i]] &&
                        this.navSources[this.bearingSourceIdxs[i]].srcId == source) {
                        this.publisher.publishBrgValidity(i, valid);
                    }
                }
            }
        }
        // TODO Unify the next two functions
        /**
         * Handle a bearing distance change.
         * @param distance The distance to the source.
         * @param source The nav source ID.
         */
        onBrgDistance(distance, source) {
            for (let i = 0; i < this.bearingSourceIdxs.length; i++) {
                if (this.navSources[this.bearingSourceIdxs[i]] &&
                    this.navSources[this.bearingSourceIdxs[i]].srcId == source &&
                    !this.navSources[this.bearingSourceIdxs[i]].isLocalizerFrequency) {
                    this.publisher.publishBrgDist(i, distance);
                }
            }
        }
        /**
         * Handle a bearing direction change.
         * @param direction The distance to the source.
         * @param source The nav source ID.
         */
        onBrgDirection(direction, source) {
            for (let i = 0; i < this.bearingSourceIdxs.length; i++) {
                if (this.navSources[this.bearingSourceIdxs[i]] &&
                    this.navSources[this.bearingSourceIdxs[i]].srcId == source &&
                    !this.navSources[this.bearingSourceIdxs[i]].isLocalizerFrequency) {
                    this.publisher.publishBrgDir(i, direction);
                }
            }
        }
        /**
         * Handle a bearing ident change.
         * @param ident The ident of the source.
         * @param source The nav source ID.
         */
        onBrgIdent(ident, source) {
            for (let i = 0; i < this.bearingSourceIdxs.length; i++) {
                if (this.navSources[this.bearingSourceIdxs[i]] &&
                    this.navSources[this.bearingSourceIdxs[i]].srcId == source) {
                    this.publisher.publishBrgIdent(i, ident, this.navSources[this.bearingSourceIdxs[i]].isLocalizerFrequency);
                    //this.publisher.publishBrgIsLoc(i, this.navSources[this.bearingSourceIdxs[i]].isLocalizerFrequency);
                    if (this.navSources[this.bearingSourceIdxs[i]].isLocalizerFrequency) {
                        this.publisher.publishBrgDir(i, null);
                        this.publisher.publishBrgDist(i, null);
                    }
                    else {
                        this.publisher.publishBrgSrc(i, source);
                    }
                }
            }
        }
        /**
         * Handle a localizer course change.
         * @param course The localizer course of the source.
         * @param source The nav source ID.
         */
        onLocalizerCourse(course, source) {
            for (let i = 0; i < this.navSources.length; i++) {
                if (this.navSources[i] && this.navSources[i].srcId == source && this.navSources[i].hasLocalizer && course !== null) {
                    const localizer = { isValid: true, course: course, source: source };
                    this.publisher.publishLocalizer(localizer, source);
                }
            }
        }
        /**
         * Handle a glideslope deviation change.
         * @param deviation The glideslope deviation of the source.
         * @param source The nav source ID.
         */
        onGlideslopeDeviation(deviation, source) {
            for (let i = 0; i < this.navSources.length; i++) {
                const navSource = this.navSources[i];
                if (navSource && navSource.srcId == source && deviation !== null && navSource.glideslopeAngle) {
                    const glideslope = { isValid: this.navSources[i].hasGlideslope, deviation: deviation, gsAngle: navSource.glideslopeAngle, source: source };
                    this.publisher.publishGlideslope(glideslope, source);
                }
            }
        }
        /**
         * Handle a glideslope angle change.
         * @param angle The glideslope angle of the source.
         * @param source The nav source ID.
         */
        onGlideslopeAngle(angle, source) {
            for (let i = 0; i < this.navSources.length; i++) {
                const navSource = this.navSources[i];
                if (navSource && navSource.srcId == source && navSource.deviation && angle !== null) {
                    const glideslope = { isValid: this.navSources[i].hasGlideslope, deviation: navSource.deviation, gsAngle: angle, source: source };
                    this.publisher.publishGlideslope(glideslope, source);
                }
            }
        }
        /**
         * Determine whether a set frequency is a localizer frequency.
         * @param frequency The frequency to evaluate.
         * @returns a bool true if the frequency is a loc freq.
         */
        frequencyIsLocalizer(frequency) {
            const roundedFreq = Math.round(frequency * 100) / 100;
            return RadioUtils.isLocalizerFrequency(roundedFreq);
        }
        /**
         * Publishers whether a set frequency is a localizer frequency.
         * @param isLoc whether the freq is a loc.
         * @param source the selected nav source.
         */
        onIsLocalizerFrequency(isLoc, source) {
            if (isLoc !== null) {
                const loc = { isLocalizer: isLoc, source: source };
                this.publisher.publishIsLocalizerFrequency(loc, source);
            }
        }
        /**
         * Publishers dme distance info.  This should be replaced by a generalization of
         * BearingDistance that provides the distance to any nav source if it has DME.
         * @param hasDme whether the radio has dme.
         * @param distance is the dme distance.
         * @param source the selected nav source.
         */
        onDme(hasDme, distance, source) {
            this.publisher.publishDmeState(hasDme ? hasDme : false, distance ? distance : -1, source);
        }
        /**
         * Gets the index of the first nav source of the given type.
         * @param type The type of nav source to find.
         * @returns The nav source index.
         */
        getFirstNavSourceIndexByType(type) {
            return this.navSources.findIndex(source => source.srcId.type === type);
        }
        /**
         * Perform events for the update loop.
         */
        onUpdate() {
            // noop
        }
    }

    /**
     * An instrument that gathers localizer and glideslope information for use by
     * the AP systems.
     *
     * Requires that the topics defined in {@link NavComEvents} are published to the event bus.
     */
    class APRadioNavInstrument {
        /**
         * Creates an instance of the APRadioNavInstrument.
         * @param bus The event bus to use with this instance.
         * @param cdiId The ID of the CDI from which this instrument sources navigation source data. Defaults to the empty
         * string (`''`).
         */
        constructor(bus, cdiId = '') {
            this.bus = bus;
            this.cdiId = cdiId;
            this.navRadioData = ArrayUtils.create(5, index => {
                index = Math.max(1, index);
                return {
                    gsLocation: new LatLongAlt(0, 0),
                    navLocation: new LatLongAlt(0, 0),
                    glideslope: this.createEmptyGlideslope({ index, type: exports.NavSourceType.Nav }),
                    localizer: this.createEmptyLocalizer({ index, type: exports.NavSourceType.Nav }),
                    cdi: this.createEmptyCdi({ index, type: exports.NavSourceType.Nav }),
                    obs: this.createEmptyObs({ index, type: exports.NavSourceType.Nav }),
                    radialError: 0,
                    magVar: 0
                };
            });
            this.currentCdiNavSourceIndex = 1;
            this.publisher = bus.getPublisher();
        }
        /** @inheritdoc */
        init() {
            const navComSubscriber = this.bus.getSubscriber();
            for (let i = 1; i < 5; i++) {
                navComSubscriber.on(`nav_glideslope_${i}`).whenChanged().handle(this.setGlideslopeValue.bind(this, i, 'isValid'));
                navComSubscriber.on(`nav_gs_lla_${i}`).handle(this.setGlideslopePosition.bind(this, i));
                navComSubscriber.on(`nav_gs_error_${i}`).whenChanged().handle(this.setGlideslopeValue.bind(this, i, 'deviation'));
                navComSubscriber.on(`nav_raw_gs_${i}`).whenChanged().handle(this.setGlideslopeValue.bind(this, i, 'gsAngle'));
                navComSubscriber.on(`nav_localizer_${i}`).whenChanged().handle(this.setLocalizerValue.bind(this, i, 'isValid'));
                navComSubscriber.on(`nav_localizer_crs_${i}`).whenChanged().handle(this.setLocalizerValue.bind(this, i, 'course'));
                navComSubscriber.on(`nav_cdi_${i}`).whenChanged().handle(this.setCDIValue.bind(this, i, 'deviation'));
                navComSubscriber.on(`nav_has_nav_${i}`).whenChanged().handle(hasNav => !hasNav && this.setCDIValue(i, 'deviation', null));
                navComSubscriber.on(`nav_obs_${i}`).whenChanged().handle(this.setOBSValue.bind(this, i, 'heading'));
                navComSubscriber.on(`nav_lla_${i}`).handle(this.setNavPosition.bind(this, i));
                navComSubscriber.on(`nav_radial_error_${i}`).whenChanged().handle(this.setRadialError.bind(this, i));
                navComSubscriber.on(`nav_magvar_${i}`).whenChanged().handle(this.setMagVar.bind(this, i));
            }
            const navEvents = this.bus.getSubscriber();
            navEvents.on(`cdi_select${CdiUtils.getEventBusTopicSuffix(this.cdiId)}`).handle(source => {
                const oldIndex = this.currentCdiNavSourceIndex;
                this.currentCdiNavSourceIndex = source.type === exports.NavSourceType.Nav ? source.index : 0;
                if (oldIndex !== this.currentCdiNavSourceIndex) {
                    const data = this.navRadioData[this.currentCdiNavSourceIndex];
                    this.publisher.pub('nav_radio_active_gs_location', data.gsLocation);
                    this.publisher.pub('nav_radio_active_nav_location', data.navLocation);
                    this.publisher.pub('nav_radio_active_glideslope', data.glideslope);
                    this.publisher.pub('nav_radio_active_localizer', data.localizer);
                    this.publisher.pub('nav_radio_active_cdi_deviation', data.cdi);
                    this.publisher.pub('nav_radio_active_obs_setting', data.obs);
                    this.publisher.pub('nav_radio_active_radial_error', data.radialError);
                    this.publisher.pub('nav_radio_active_magvar', data.magVar);
                }
            });
        }
        /** @inheritdoc */
        onUpdate() {
            // noop
        }
        /**
         * Sets a value in a nav radio glideslope.
         * @param index The index of the nav radio.
         * @param field The field to set.
         * @param value The value to set the field to.
         */
        setGlideslopeValue(index, field, value) {
            this.navRadioData[index].glideslope[field] = value;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_glideslope', this.navRadioData[index].glideslope);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_glideslope_1', this.navRadioData[index].glideslope);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_glideslope_2', this.navRadioData[index].glideslope);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_glideslope_3', this.navRadioData[index].glideslope);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_glideslope_4', this.navRadioData[index].glideslope);
                    break;
            }
        }
        /**
         * Sends the current glideslope's LLA position.
         * @param index The index of the nav radio.
         * @param lla The LLA to send.
         */
        setGlideslopePosition(index, lla) {
            this.navRadioData[index].gsLocation = lla;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_gs_location', lla);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_gs_location_1', this.navRadioData[index].gsLocation);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_gs_location_2', this.navRadioData[index].gsLocation);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_gs_location_3', this.navRadioData[index].gsLocation);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_gs_location_4', this.navRadioData[index].gsLocation);
                    break;
            }
        }
        /**
         * Sends the current nav's LLA position.
         * @param index The index of the nav radio.
         * @param lla The LLA to send.
         */
        setNavPosition(index, lla) {
            this.navRadioData[index].navLocation = lla;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_nav_location', lla);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_nav_location_1', this.navRadioData[index].navLocation);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_nav_location_2', this.navRadioData[index].navLocation);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_nav_location_3', this.navRadioData[index].navLocation);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_nav_location_4', this.navRadioData[index].navLocation);
                    break;
            }
        }
        /**
         * Sets a value in a nav radio localizer.
         * @param index The index of the nav radio.
         * @param field The field to set.
         * @param value The value to set the field to.
         */
        setLocalizerValue(index, field, value) {
            this.navRadioData[index].localizer[field] = value;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_localizer', this.navRadioData[index].localizer);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_localizer_1', this.navRadioData[index].localizer);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_localizer_2', this.navRadioData[index].localizer);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_localizer_3', this.navRadioData[index].localizer);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_localizer_4', this.navRadioData[index].localizer);
                    break;
            }
        }
        /**
         * Sets a value in a nav radio localizer.
         * @param index The index of the nav radio.
         * @param field The field to set.
         * @param value The value to set the field to.
         */
        setCDIValue(index, field, value) {
            this.navRadioData[index].cdi[field] = value;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_cdi_deviation', this.navRadioData[index].cdi);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_cdi_1', this.navRadioData[index].cdi);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_cdi_2', this.navRadioData[index].cdi);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_cdi_3', this.navRadioData[index].cdi);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_cdi_4', this.navRadioData[index].cdi);
                    break;
            }
        }
        /**
         * Sets a value in a nav radio localizer.
         * @param index The index of the nav radio.
         * @param field The field to set.
         * @param value The value to set the field to.
         */
        setOBSValue(index, field, value) {
            this.navRadioData[index].obs[field] = value;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_obs_setting', this.navRadioData[index].obs);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_obs_1', this.navRadioData[index].obs);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_obs_2', this.navRadioData[index].obs);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_obs_3', this.navRadioData[index].obs);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_obs_4', this.navRadioData[index].obs);
                    break;
            }
        }
        /**
         * Sets the radial error of a nav radio signal source.
         * @param index The index of the nav radio.
         * @param radialError The radial error to set.
         */
        setRadialError(index, radialError) {
            this.navRadioData[index].radialError = radialError;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_radial_error', radialError);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_radial_error_1', this.navRadioData[index].radialError);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_radial_error_2', this.navRadioData[index].radialError);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_radial_error_3', this.navRadioData[index].radialError);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_radial_error_4', this.navRadioData[index].radialError);
                    break;
            }
        }
        /**
         * Sets the magnetic variation of a nav radio signal source.
         * @param index The index of the nav radio.
         * @param magVar The magvar to set.
         */
        setMagVar(index, magVar) {
            magVar = NavMath.normalizeHeading(-magVar + 180) % 360 - 180;
            this.navRadioData[index].magVar = magVar;
            if (this.currentCdiNavSourceIndex === index) {
                this.publisher.pub('nav_radio_active_magvar', magVar);
            }
            switch (index) {
                case 1:
                    this.publisher.pub('nav_radio_magvar_1', this.navRadioData[index].magVar);
                    break;
                case 2:
                    this.publisher.pub('nav_radio_magvar_2', this.navRadioData[index].magVar);
                    break;
                case 3:
                    this.publisher.pub('nav_radio_magvar_3', this.navRadioData[index].magVar);
                    break;
                case 4:
                    this.publisher.pub('nav_radio_magvar_4', this.navRadioData[index].magVar);
                    break;
            }
        }
        /**
         * Creates an empty localizer data.
         * @param id The nav source ID.
         * @returns New empty localizer data.
         */
        createEmptyLocalizer(id) {
            return {
                isValid: false,
                course: 0,
                source: id
            };
        }
        /**
         * Creates an empty glideslope data.
         * @param id The nav source ID.
         * @returns New empty glideslope data.
         */
        createEmptyGlideslope(id) {
            return {
                isValid: false,
                gsAngle: 0,
                deviation: 0,
                source: id
            };
        }
        /**
         * Creates an empty CDI data.
         * @param id The nav source ID.
         * @returns New empty CDI data.
         */
        createEmptyCdi(id) {
            return {
                deviation: 0,
                source: id
            };
        }
        /**
         * Creates an empty OBS data.
         * @param id The nav source ID.
         * @returns New empty OBS data.
         */
        createEmptyObs(id) {
            return {
                heading: 0,
                source: id
            };
        }
    }

    /**
     * InstrumentBackplane provides a common control point for aggregating and
     * managing any number of publishers.  This can be used as an "update loop"
     * corral", amongst other things.
     */
    class InstrumentBackplane {
        /**
         * Create an InstrumentBackplane
         */
        constructor() {
            this.publishers = new Map();
            this.instruments = new Map();
        }
        /**
         * Initialize all the things. This is initially just a proxy for the
         * private initPublishers() and initInstruments() methods.
         *
         * This should be simplified.
         */
        init() {
            this.initPublishers();
            this.initInstruments();
        }
        /**
         * Update all the things.  This is initially just a proxy for the private
         * updatePublishers() and updateInstruments() methods.
         *
         * This should be simplified.
         */
        onUpdate() {
            this.updatePublishers();
            this.updateInstruments();
        }
        /**
         * Add a publisher to this backplane.
         * @param name A symbolic name for the publisher for reference.
         * @param publisher The publisher to add.
         * @param override Whether to override any existing publishers added to this backplane under the same name. If
         * `true`, any existing publisher with the same name will removed from this backplane and the new one added in its
         * place. If `false`, the new publisher will not be added if this backplane already has a publisher with the same
         * name or a publisher of the same type. Defaults to `false`.
         */
        addPublisher(name, publisher, override = false) {
            if (override || !InstrumentBackplane.checkAlreadyExists(name, publisher, this.publishers)) {
                this.publishers.set(name, publisher);
            }
        }
        /**
         * Add an instrument to this backplane.
         * @param name A symbolic name for the instrument for reference.
         * @param instrument The instrument to add.
         * @param override Whether to override any existing instruments added to this backplane under the same name. If
         * `true`, any existing instrument with the same name will removed from this backplane and the new one added in its
         * place. If `false`, the new instrument will not be added if this backplane already has an instrument with the same
         * name or an instrument of the same type. Defaults to `false`.
         */
        addInstrument(name, instrument, override = false) {
            if (override || !InstrumentBackplane.checkAlreadyExists(name, instrument, this.instruments)) {
                this.instruments.set(name, instrument);
            }
        }
        /**
         * Gets a publisher from this backplane.
         * @param name The name of the publisher to get.
         * @returns The publisher in this backplane with the specified name, or `undefined` if there is no such publisher.
         */
        getPublisher(name) {
            return this.publishers.get(name);
        }
        /**
         * Gets an instrument from this backplane.
         * @param name The name of the instrument to get.
         * @returns The instrument in this backplane with the specified name, or `undefined` if there is no such instrument.
         */
        getInstrument(name) {
            return this.instruments.get(name);
        }
        /**
         * Checks for duplicate publishers or instruments of the same name or type.
         * @param name the name of the publisher or instrument
         * @param objToCheck the object to check
         * @param map the map to check
         * @returns true if the object is already in the map
         */
        static checkAlreadyExists(name, objToCheck, map) {
            if (map.has(name)) {
                console.warn(`${name} already exists in backplane.`);
                return true;
            }
            // check if there already is a publisher with the same type
            for (const p of map.values()) {
                if (p.constructor === objToCheck.constructor) {
                    console.warn(`${name} already exists in backplane.`);
                    return true;
                }
            }
            return false;
        }
        /**
         * Initialize all of the publishers that you hold.
         */
        initPublishers() {
            for (const publisher of this.publishers.values()) {
                publisher.startPublish();
            }
        }
        /**
         * Initialize all of the instruments that you hold.
         */
        initInstruments() {
            for (const instrument of this.instruments.values()) {
                instrument.init();
            }
        }
        /**
         * Update all of the publishers that you hold.
         */
        updatePublishers() {
            for (const publisher of this.publishers.values()) {
                publisher.onUpdate();
            }
        }
        /**
         * Update all of the instruments that you hold.
         */
        updateInstruments() {
            for (const instrument of this.instruments.values()) {
                instrument.onUpdate();
            }
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A publisher for Brake information.
     */
    class BrakeSimvarPublisher extends SimVarPublisher {
        /**
         * Create a BrakePublisher.
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer = undefined) {
            const simvars = new Map([
                ['brake_position_left', { name: 'BRAKE LEFT POSITION', type: exports.SimVarValueType.Percent }],
                ['brake_position_right', { name: 'BRAKE RIGHT POSITION', type: exports.SimVarValueType.Percent }],
                ['brake_position_left_raw', { name: 'BRAKE LEFT POSITION EX1', type: exports.SimVarValueType.Percent }],
                ['brake_position_right_raw', { name: 'BRAKE RIGHT POSITION EX1', type: exports.SimVarValueType.Percent }],
                ['left_wheel_rpm', { name: 'LEFT WHEEL RPM', type: exports.SimVarValueType.RPM }],
                ['right_wheel_rpm', { name: 'RIGHT WHEEL RPM', type: exports.SimVarValueType.RPM }],
                ['parking_brake_set', { name: 'BRAKE PARKING POSITION', type: exports.SimVarValueType.Bool }],
                ['autobrake_switch_pos', { name: 'AUTO BRAKE SWITCH CB', type: exports.SimVarValueType.Number }],
                ['autobrake_active', { name: 'AUTOBRAKES ACTIVE', type: exports.SimVarValueType.Bool }],
            ]);
            super(simvars, bus, pacer);
        }
        /** @inheritdoc */
        onUpdate() {
            super.onUpdate();
        }
    }

    /**
     * A publisher of clock events.
     */
    class ClockPublisher extends BasePublisher {
        /**
         * Creates a new instance of ClockPublisher.
         * @param bus The event bus.
         * @param pacer An optional pacer to control the rate of publishing.
         */
        constructor(bus, pacer) {
            super(bus, pacer);
            this.registeredSimVarIds = {
                absoluteTime: SimVar.GetRegisteredId('E:ABSOLUTE TIME', exports.SimVarValueType.Seconds, ''),
                simulationTime: SimVar.GetRegisteredId('E:SIMULATION TIME', exports.SimVarValueType.Seconds, ''),
            };
            this.needPublishRealTime = false;
            this.hiFreqTopicsToPublish = {
                realTime: false,
                simTime: false,
                activeSimDuration: false
            };
            this.simVarPublisher = new SimVarPublisher([
                ['simTime', { name: 'E:ABSOLUTE TIME', type: exports.SimVarValueType.Seconds, map: ClockPublisher.absoluteTimeToUNIXTime }],
                ['activeSimDuration', { name: 'E:SIMULATION TIME', type: exports.SimVarValueType.Seconds, map: seconds => seconds * 1000 }],
                ['simRate', { name: 'E:SIMULATION RATE', type: exports.SimVarValueType.Number }],
                ['zulu_sunrise', { name: 'E:ZULU SUNRISE TIME', type: exports.SimVarValueType.Seconds }],
                ['zulu_sunset', { name: 'E:ZULU SUNSET TIME', type: exports.SimVarValueType.Seconds }],
            ], bus, pacer);
            if (this.bus.getTopicSubscriberCount('realTime') > 0) {
                this.needPublishRealTime = true;
            }
            if (this.bus.getTopicSubscriberCount('realTimeHiFreq') > 0) {
                this.hiFreqTopicsToPublish.realTime = true;
            }
            if (this.bus.getTopicSubscriberCount('simTimeHiFreq') > 0) {
                this.hiFreqTopicsToPublish.simTime = true;
            }
            if (this.bus.getTopicSubscriberCount('activeSimDurationHiFreq') > 0) {
                this.hiFreqTopicsToPublish.activeSimDuration = true;
            }
            this.bus.getSubscriber().on('event_bus_topic_first_sub').handle(this.onTopicFirstSub.bind(this));
        }
        /**
         * Responds to when a topic is first subscribed to on the event bus.
         * @param topic The topic that was subscribed to.
         */
        onTopicFirstSub(topic) {
            switch (topic) {
                case 'realTime':
                    if (!this.needPublishRealTime) {
                        this.needPublishRealTime = true;
                        if (this.publishActive) {
                            this.publish('realTime', Date.now());
                        }
                    }
                    break;
                case 'realTimeHiFreq':
                    if (!this.hiFreqTopicsToPublish.realTime) {
                        this.hiFreqTopicsToPublish.realTime = true;
                        if (this.publishActive) {
                            this.publish('realTimeHiFreq', Date.now());
                        }
                    }
                    break;
                case 'simTimeHiFreq':
                    if (!this.hiFreqTopicsToPublish.simTime) {
                        this.hiFreqTopicsToPublish.simTime = true;
                        if (this.publishActive) {
                            this.publish('simTimeHiFreq', ClockPublisher.absoluteTimeToUNIXTime(SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.absoluteTime)));
                        }
                    }
                    break;
                case 'activeSimDurationHiFreq':
                    if (!this.hiFreqTopicsToPublish.activeSimDuration) {
                        this.hiFreqTopicsToPublish.activeSimDuration = true;
                        if (this.publishActive) {
                            this.publish('activeSimDurationHiFreq', SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.simulationTime) * 1000);
                        }
                    }
                    break;
            }
        }
        /** @inheritdoc */
        startPublish() {
            super.startPublish();
            this.simVarPublisher.startPublish();
            if (this.hiFreqInterval === undefined) {
                this.hiFreqInterval = setInterval(this.publishHiFreq.bind(this), 0);
            }
        }
        /** @inheritdoc */
        stopPublish() {
            super.stopPublish();
            this.simVarPublisher.stopPublish();
            if (this.hiFreqInterval !== undefined) {
                clearInterval(this.hiFreqInterval);
                this.hiFreqInterval = undefined;
            }
        }
        /** @inheritdoc */
        onUpdate() {
            if (this.needPublishRealTime) {
                this.publish('realTime', Date.now());
            }
            this.simVarPublisher.onUpdate();
        }
        /**
         * Publishes the high-frequency topics.
         */
        publishHiFreq() {
            if (this.hiFreqTopicsToPublish.realTime) {
                this.publish('realTimeHiFreq', Date.now());
            }
            if (this.hiFreqTopicsToPublish.simTime) {
                this.publish('simTimeHiFreq', ClockPublisher.absoluteTimeToUNIXTime(SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.absoluteTime)));
            }
            if (this.hiFreqTopicsToPublish.activeSimDuration) {
                this.publish('activeSimDurationHiFreq', SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.simulationTime) * 1000);
            }
        }
        /**
         * Converts the sim's absolute time to a UNIX timestamp. The sim's absolute time value is equivalent to a .NET
         * DateTime.Ticks value (epoch = 00:00:00 01 Jan 0001).
         * @param absoluteTime an absolute time value, in units of seconds.
         * @returns the UNIX timestamp corresponding to the absolute time value.
         */
        static absoluteTimeToUNIXTime(absoluteTime) {
            return (absoluteTime - 62135596800) * 1000;
        }
    }
    /**
     * A clock which keeps track of real-world and sim time.
     */
    class Clock {
        /**
         * Constructor.
         * @param bus The event bus to use to publish events from this clock.
         */
        constructor(bus) {
            this.publisher = new ClockPublisher(bus);
        }
        /**
         * Initializes this clock.
         */
        init() {
            this.publisher.startPublish();
        }
        /**
         * Updates this clock.
         */
        onUpdate() {
            this.publisher.onUpdate();
        }
    }

    /**
     * A publisher for control surfaces information.
     */
    class ControlSurfacesPublisher extends SimVarPublisher {
        /**
         * Create an ControlSurfacesPublisher.
         * @param bus The EventBus to publish to.
         * @param gearCount The number of landing gear to support.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, gearCount, pacer) {
            const nonIndexedSimVars = [
                ['flaps_handle_index', { name: 'FLAPS HANDLE INDEX', type: exports.SimVarValueType.Number }],
                ['flaps_left_angle', { name: 'TRAILING EDGE FLAPS LEFT ANGLE', type: exports.SimVarValueType.Degree }],
                ['flaps_right_angle', { name: 'TRAILING EDGE FLAPS RIGHT ANGLE', type: exports.SimVarValueType.Degree }],
                ['flaps_left_percent', { name: 'TRAILING EDGE FLAPS LEFT PERCENT', type: exports.SimVarValueType.Percent }],
                ['flaps_right_percent', { name: 'TRAILING EDGE FLAPS RIGHT PERCENT', type: exports.SimVarValueType.Percent }],
                ['slats_left_angle', { name: 'LEADING EDGE FLAPS LEFT ANGLE', type: exports.SimVarValueType.Degree }],
                ['slats_right_angle', { name: 'LEADING EDGE FLAPS RIGHT ANGLE', type: exports.SimVarValueType.Degree }],
                ['slats_left_percent', { name: 'LEADING EDGE FLAPS LEFT PERCENT', type: exports.SimVarValueType.Percent }],
                ['slats_right_percent', { name: 'LEADING EDGE FLAPS RIGHT PERCENT', type: exports.SimVarValueType.Percent }],
                ['spoilers_left_percent', { name: 'SPOILERS LEFT POSITION', type: exports.SimVarValueType.Percent }],
                ['spoilers_right_percent', { name: 'SPOILERS RIGHT POSITION', type: exports.SimVarValueType.Percent }],
                ['spoilers_without_spoilerons_left_percent', { name: 'SPOILERS WITHOUT SPOILERONS LEFT POSITION', type: exports.SimVarValueType.Percent }],
                ['elevator_trim_angle', { name: 'ELEVATOR TRIM POSITION', type: exports.SimVarValueType.Degree }],
                ['elevator_trim_pct', { name: 'ELEVATOR TRIM PCT', type: exports.SimVarValueType.Percent }],
                ['elevator_trim_neutral_pct', { name: 'AIRCRAFT ELEVATOR TRIM NEUTRAL', type: exports.SimVarValueType.Percent }],
                ['aileron_trim_angle', { name: 'AILERON TRIM', type: exports.SimVarValueType.Degree }],
                ['aileron_trim_pct', { name: 'AILERON TRIM PCT', type: exports.SimVarValueType.Percent }],
                ['rudder_trim_angle', { name: 'RUDDER TRIM', type: exports.SimVarValueType.Degree }],
                ['rudder_trim_pct', { name: 'RUDDER TRIM PCT', type: exports.SimVarValueType.Percent }],
                ['aileron_left_percent', { name: 'AILERON LEFT DEFLECTION PCT', type: exports.SimVarValueType.Percent }],
                ['aileron_right_percent', { name: 'AILERON RIGHT DEFLECTION PCT', type: exports.SimVarValueType.Percent }],
                ['elevator_percent', { name: 'ELEVATOR DEFLECTION PCT', type: exports.SimVarValueType.Percent }],
                ['elevator_position', { name: 'ELEVATOR POSITION', type: exports.SimVarValueType.Percent }],
                ['rudder_percent', { name: 'RUDDER DEFLECTION PCT', type: exports.SimVarValueType.Percent }],
                ['gear_handle_position', { name: 'GEAR HANDLE POSITION', type: exports.SimVarValueType.PercentOver100 }],
            ];
            const gearIndexedSimVars = [
                ['gear_position', { name: 'GEAR POSITION', type: exports.SimVarValueType.Number }],
                ['gear_is_on_ground', { name: 'GEAR IS ON GROUND', type: exports.SimVarValueType.Bool }]
            ];
            const simvars = new Map(nonIndexedSimVars);
            // set un-indexed simvar topics to pull from index 0
            for (const [topic, simvar] of [...gearIndexedSimVars]) {
                simvars.set(`${topic}`, {
                    name: `${simvar.name}:0`,
                    type: simvar.type,
                    map: simvar.map
                });
            }
            // add landing gear indexed simvar topics
            // HINT: for some reason index 0 is nose. not 1-based.
            gearCount = Math.max(gearCount, 1);
            for (let i = 0; i < gearCount; i++) {
                for (const [topic, simvar] of gearIndexedSimVars) {
                    simvars.set(`${topic}_${i}`, {
                        name: `${simvar.name}:${i}`,
                        type: simvar.type,
                        map: simvar.map
                    });
                }
            }
            super(simvars, bus, pacer);
        }
    }

    /**
     * A publisher for control surfaces information.
     */
    class HydraulicsPublisher extends SimVarPublisher {
        /**
         * Create an HydraulicsPublisher.
         * @param bus The EventBus to publish to.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pacer) {
            super([
                ['hyd_pump_switch_state', { name: 'HYDRAULIC SWITCH:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['hyd_pressure', { name: 'HYDRAULIC PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true, defaultIndex: null }],
                ['hyd_reservoir_perc', { name: 'HYDRAULIC RESERVOIR PERCENT:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
                // FS2024 New Hydraulics System
                ['hyd_pump_active', { name: 'HYDRAULIC PUMP ACTIVE:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['hyd_pump_pressure', { name: 'HYDRAULIC PUMP PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true, defaultIndex: null }],
                ['hyd_reservoir_quantity_pct', { name: 'HYDRAULIC RESERVOIR PERCENT:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
                ['hyd_reservoir_quantity', { name: 'HYDRAULIC RESERVOIR QUANTITY:#index#', type: exports.SimVarValueType.Liters, indexed: true, defaultIndex: null }],
                ['hyd_reservoir_pressure', { name: 'HYDRAULIC RESERVOIR PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true, defaultIndex: null }],
                ['hyd_accumulator_quantity', { name: 'HYDRAULIC ACCUMULATOR QUANTITY:#index#', type: exports.SimVarValueType.Liters, indexed: true, defaultIndex: null }],
                ['hyd_accumulator_pressure', { name: 'HYDRAULIC ACCUMULATOR PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true, defaultIndex: null }],
            ], bus, pacer);
        }
    }

    /**
     * A publisher for electrical information.
     */
    class ElectricalPublisher extends BasePublisher {
        /**
         * Create an ElectricalPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer) {
            super(bus, pacer);
            this.needPublishAv1Bus = false;
            this.needPublishAv2Bus = false;
            this.flightStarted = false;
            this.simVarPublisher = new SimVarPublisher([
                // ---- BUS ----
                ['elec_bus_v', { name: 'ELECTRICAL BUS VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true, defaultIndex: null }],
                ['elec_bus_a', { name: 'ELECTRICAL BUS AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true, defaultIndex: null }],
                ['elec_bus_main_v', { name: 'ELECTRICAL MAIN BUS VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true, defaultIndex: null }],
                ['elec_bus_main_a', { name: 'ELECTRICAL MAIN BUS AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true, defaultIndex: null }],
                ['elec_bus_avionics_v', { name: 'ELECTRICAL AVIONICS BUS VOLTAGE', type: exports.SimVarValueType.Volts }],
                ['elec_bus_avionics_a', { name: 'ELECTRICAL AVIONICS BUS AMPS', type: exports.SimVarValueType.Amps }],
                ['elec_bus_genalt_v', { name: 'ELECTRICAL GENALT BUS VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true, defaultIndex: null }],
                ['elec_bus_genalt_a', { name: 'ELECTRICAL GENALT BUS AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true, defaultIndex: null }],
                // ---- CIRCUIT ----
                ['elec_circuit_on', { name: 'CIRCUIT ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_circuit_switch_on', { name: 'CIRCUIT SWITCH ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_circuit_v', { name: 'ELECTRICAL CIRCUIT VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true, defaultIndex: null }],
                ['elec_circuit_a', { name: 'ELECTRICAL CIRCUIT AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true, defaultIndex: null }],
                ['elec_circuit_com_on', { name: 'CIRCUIT COM ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_circuit_nav_on', { name: 'CIRCUIT NAV ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_circuit_avionics_on', { name: 'CIRCUIT AVIONICS ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_circuit_navcom1_on', { name: 'CIRCUIT NAVCOM1 ON', type: exports.SimVarValueType.Bool }],
                ['elec_circuit_navcom2_on', { name: 'CIRCUIT NAVCOM2 ON', type: exports.SimVarValueType.Bool }],
                ['elec_circuit_navcom3_on', { name: 'CIRCUIT NAVCOM3 ON', type: exports.SimVarValueType.Bool }],
                // ---- LINE ----
                ['elec_line_connection_on', { name: 'LINE CONNECTION ON:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_line_breaker_pulled', { name: 'LINE BREAKER PULLED:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                // ---- BATTERY ----
                ['elec_master_battery', { name: 'ELECTRICAL MASTER BATTERY:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['elec_bat_load', { name: 'ELECTRICAL BATTERY LOAD:#index#', type: exports.SimVarValueType.Amps, indexed: true }],
                ['elec_bat_v', { name: 'ELECTRICAL BATTERY VOLTAGE:#index#', type: exports.SimVarValueType.Amps, indexed: true }],
                ['elec_bat_soc', { name: 'ELECTRICAL BATTERY ESTIMATED CAPACITY PCT:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                // ---- GENERATOR/ALTERNATOR ----
                ['elec_gen_switch_on', { name: 'ELECTRICAL GENERATOR SWITCH:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_gen_active', { name: 'ELECTRICAL GENERATOR ACTIVE:#index#', type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                ['elec_gen_v', { name: 'ELECTRICAL GENERATOR VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true, defaultIndex: null }],
                ['elec_gen_a', { name: 'ELECTRICAL GENERATOR AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true, defaultIndex: null }],
                ['elec_eng_gen_switch', { name: 'GENERAL ENG MASTER ALTERNATOR:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['elec_apu_gen_switch', { name: 'APU GENERATOR SWITCH:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['elec_apu_gen_active', { name: 'APU GENERATOR ACTIVE:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                // ---- EXTERNAL POWER ----
                ['elec_ext_power_available', { name: 'EXTERNAL POWER AVAILABLE:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['elec_ext_power_on', { name: 'EXTERNAL POWER ON:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['elec_ext_power_v', { name: 'ELECTRICAL EXTERNAL POWER VOLTAGE:#index#', type: exports.SimVarValueType.Volts, indexed: true }],
                ['elec_ext_power_a', { name: 'ELECTRICAL EXTERNAL POWER AMPS:#index#', type: exports.SimVarValueType.Amps, indexed: true }],
            ], bus, pacer);
            for (const topic of ['elec_av1_bus', 'elec_av2_bus']) {
                if (bus.getTopicSubscriberCount(topic) > 0) {
                    this.onTopicSubscribed(topic);
                }
            }
            bus.getSubscriber().on('event_bus_topic_first_sub').handle(this.onTopicSubscribed.bind(this));
            // When not starting cold and dark (on runway or in air), electrical power simvars are not properly initialized
            // during loading, so we will ignore all power data until the game enters briefing state.
            const gameStateSub = GameStateProvider.get().sub(state => {
                if (state === GameState.briefing || state === GameState.ingame) {
                    gameStateSub.destroy();
                    this.flightStarted = true;
                }
            }, false, true);
            gameStateSub.resume(true);
        }
        /**
         * Responds to when a topic is first subscribed to on the event bus.
         * @param topic The subscribed topic.
         */
        onTopicSubscribed(topic) {
            if (topic === 'elec_av1_bus') {
                this.needPublishAv1Bus = true;
                if (this.publishActive && this.av1BusLogic) {
                    this.publishAvBus(topic, this.av1BusLogic);
                }
            }
            else if (topic === 'elec_av2_bus') {
                this.needPublishAv2Bus = true;
                if (this.publishActive && this.av2BusLogic) {
                    this.publishAvBus(topic, this.av2BusLogic);
                }
            }
        }
        /** @inheritDoc */
        startPublish() {
            super.startPublish();
            this.simVarPublisher.startPublish();
        }
        /** @inheritDoc */
        stopPublish() {
            super.stopPublish();
            this.simVarPublisher.stopPublish();
        }
        /** @inheritDoc */
        onUpdate() {
            if (!this.flightStarted) {
                return;
            }
            this.simVarPublisher.onUpdate();
            if (this.av1BusLogic && this.needPublishAv1Bus) {
                this.publishAvBus('elec_av1_bus', this.av1BusLogic);
            }
            if (this.av2BusLogic && this.needPublishAv2Bus) {
                this.publishAvBus('elec_av2_bus', this.av2BusLogic);
            }
        }
        /**
         * Publishes an avionics bus topic.
         * @param topic The topic to publish.
         * @param logicElement The XML logic element from which to retrieve the value to publish to the topic.
         */
        publishAvBus(topic, logicElement) {
            this.publish(topic, logicElement.getValue() !== 0);
        }
        /**
         * Sets the logic element to use for the avionics 1 bus.
         * @param logicElement The logic element to use.
         */
        setAv1Bus(logicElement) {
            this.av1BusLogic = logicElement;
        }
        /**
         * Sets the logic element to use for the avionics 2 bus.
         * @param logicElement The logic element to use.
         */
        setAv2Bus(logicElement) {
            this.av2BusLogic = logicElement;
        }
    }

    /** The type of engine (see `ENGINE TYPE` simvar). */
    exports.EngineType = void 0;
    (function (EngineType) {
        EngineType[EngineType["Piston"] = 0] = "Piston";
        EngineType[EngineType["Jet"] = 1] = "Jet";
        EngineType[EngineType["None"] = 2] = "None";
        EngineType[EngineType["HeloTurbine"] = 3] = "HeloTurbine";
        EngineType[EngineType["Unsupported"] = 4] = "Unsupported";
        EngineType[EngineType["Turboprop"] = 5] = "Turboprop";
    })(exports.EngineType || (exports.EngineType = {}));
    /** A publisher for Engine information. */
    class EISPublisher extends BasePublisher {
        /**
         * Create an EISPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer) {
            super(bus, pacer);
            this.needPublishTotalFuelFlow = false;
            this.totalFuelFlowSimVarIds = [];
            const isUsingAdvancedFuelSystem = SimVar.GetSimVarValue('NEW FUEL SYSTEM', exports.SimVarValueType.Bool) !== 0;
            const totalUnusableFuelGal = SimVar.GetSimVarValue('UNUSABLE FUEL TOTAL QUANTITY', exports.SimVarValueType.GAL);
            const totalUnusableFuelLb = UnitType.GALLON_FUEL.convertTo(totalUnusableFuelGal, UnitType.POUND);
            this.simVarPublisher = new SimVarPublisher([
                ['vac', { name: 'SUCTION PRESSURE', type: exports.SimVarValueType.InHG }],
                ['fuel_total', { name: 'FUEL TOTAL QUANTITY', type: exports.SimVarValueType.GAL, map: isUsingAdvancedFuelSystem ? v => v + totalUnusableFuelGal : undefined }],
                ['fuel_total_weight', { name: 'FUEL TOTAL QUANTITY WEIGHT', type: exports.SimVarValueType.Pounds, map: isUsingAdvancedFuelSystem ? v => v + totalUnusableFuelGal : undefined }],
                ['fuel_usable_total', { name: 'FUEL TOTAL QUANTITY', type: exports.SimVarValueType.GAL, map: isUsingAdvancedFuelSystem ? undefined : v => Math.max(v - totalUnusableFuelGal, 0) }],
                ['fuel_usable_total_weight', { name: 'FUEL TOTAL QUANTITY WEIGHT', type: exports.SimVarValueType.Pounds, map: isUsingAdvancedFuelSystem ? undefined : v => Math.max(v - totalUnusableFuelLb, 0) }],
                ['fuel_left', { name: 'FUEL LEFT QUANTITY', type: exports.SimVarValueType.GAL }],
                ['fuel_right', { name: 'FUEL RIGHT QUANTITY', type: exports.SimVarValueType.GAL }],
                ['fuel_left_main', { name: 'FUEL TANK LEFT MAIN QUANTITY', type: exports.SimVarValueType.GAL }],
                ['fuel_left_main_pct', { name: 'FUEL TANK LEFT MAIN LEVEL', type: exports.SimVarValueType.Percent }],
                ['fuel_right_main', { name: 'FUEL TANK RIGHT MAIN QUANTITY', type: exports.SimVarValueType.GAL }],
                ['fuel_right_main_pct', { name: 'FUEL TANK RIGHT MAIN LEVEL', type: exports.SimVarValueType.Percent }],
                ['fuel_center', { name: 'FUEL TANK CENTER QUANTITY', type: exports.SimVarValueType.GAL }],
                ['fuel_weight_per_gallon', { name: 'FUEL WEIGHT PER GALLON', type: exports.SimVarValueType.LBS }],
                ['fuel_tank_selector_state_1', { name: 'FUEL TANK SELECTOR:1', type: exports.SimVarValueType.Number }],
                ['fuel_tank_selector_state_2', { name: 'FUEL TANK SELECTOR:2', type: exports.SimVarValueType.Number }],
                ['fuel_tank_selector_state_3', { name: 'FUEL TANK SELECTOR:3', type: exports.SimVarValueType.Number }],
                ['fuel_tank_selector_state_4', { name: 'FUEL TANK SELECTOR:4', type: exports.SimVarValueType.Number }],
                ['eng_hours_1', { name: 'GENERAL ENG ELAPSED TIME:1', type: exports.SimVarValueType.Hours }],
                ['apu_pct', { name: 'APU PCT RPM', type: exports.SimVarValueType.Percent }],
                ['apu_pct_starter', { name: 'APU PCT STARTER', type: exports.SimVarValueType.Percent }],
                ['apu_switch', { name: 'APU SWITCH', type: exports.SimVarValueType.Bool }],
                ['throttle_lower_limit', { name: 'THROTTLE LOWER LIMIT', type: exports.SimVarValueType.Number }],
                ['rpm', { name: 'GENERAL ENG RPM:#index#', type: exports.SimVarValueType.RPM, indexed: true }],
                ['prop_rpm', { name: 'PROP RPM:#index#', type: exports.SimVarValueType.RPM, indexed: true }],
                ['n1', { name: 'TURB ENG CORRECTED N1:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                ['n1_uncorrected', { name: 'TURB ENG N1:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                ['n2', { name: 'TURB ENG CORRECTED N2:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                ['n2_uncorrected', { name: 'TURB ENG N2:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                ['torque', { name: 'TURB ENG MAX TORQUE PERCENT:#index#', type: exports.SimVarValueType.Percent, indexed: true }],
                ['fuel_flow', { name: 'ENG FUEL FLOW GPH:#index#', type: exports.SimVarValueType.GPH, indexed: true }],
                ['recip_ff', { name: 'RECIP ENG FUEL FLOW:#index#', type: exports.SimVarValueType.PPH, indexed: true }],
                ['oil_press', { name: 'ENG OIL PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true }],
                ['oil_temp', { name: 'ENG OIL TEMPERATURE:#index#', type: exports.SimVarValueType.Farenheit, indexed: true }],
                ['itt', { name: 'TURB ENG ITT:#index#', type: exports.SimVarValueType.Celsius, indexed: true }],
                ['egt', { name: 'ENG EXHAUST GAS TEMPERATURE:#index#', type: exports.SimVarValueType.Farenheit, indexed: true }],
                ['eng_hyd_press', { name: 'ENG HYDRAULIC PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true }],
                ['eng_starter_on', { name: 'GENERAL ENG STARTER:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['eng_combustion', { name: 'GENERAL ENG COMBUSTION:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['eng_ignition_switch_state', { name: 'TURB ENG IGNITION SWITCH EX1:#index#', type: exports.SimVarValueType.Number, indexed: true }],
                ['eng_igniting', { name: 'TURB ENG IS IGNITING:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['eng_fuel_pump_on', { name: 'GENERAL ENG FUEL PUMP ON:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['eng_fuel_pump_switch_state', { name: 'GENERAL ENG FUEL PUMP SWITCH EX1:#index#', type: exports.SimVarValueType.Number, indexed: true }],
                ['eng_vibration', { name: 'ENG VIBRATION:#index#', type: exports.SimVarValueType.Number, indexed: true }],
                ['fuel_flow_pph', { name: 'ENG FUEL FLOW PPH:#index#', type: exports.SimVarValueType.PPH, indexed: true }],
                ['torque_moment', { name: 'ENG TORQUE:#index#', type: exports.SimVarValueType.FtLb, indexed: true }],
                ['eng_manifold_pressure', { name: 'ENG MANIFOLD PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true }],
                ['reverse_thrust_engaged', { name: 'GENERAL ENG REVERSE THRUST ENGAGED:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['cylinder_head_temp_avg', { name: 'ENG CYLINDER HEAD TEMPERATURE:#index#', type: exports.SimVarValueType.Farenheit, indexed: true }],
                ['recip_turbine_inlet_temp_avg', { name: 'RECIP ENG TURBINE INLET TEMPERATURE:#index#', type: exports.SimVarValueType.Farenheit, indexed: true }],
                ['turbine_inlet_temp', { name: 'TURB ENG INLET TEMPERATURE:#index#', type: exports.SimVarValueType.Celsius, indexed: true }],
                ['jet_net_thrust', { name: 'TURB ENG JET THRUST:#index#', type: exports.SimVarValueType.Pounds, indexed: true }],
                ['eng_fire', { name: 'ENG ON FIRE:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['eng_starter_active', { name: 'GENERAL ENG STARTER ACTIVE:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
            ], bus, pacer);
            this.engineCount = SimVar.GetSimVarValue('NUMBER OF ENGINES', exports.SimVarValueType.Number);
            if (bus.getTopicSubscriberCount('fuel_flow_total') > 0) {
                this.onTopicSubscribed('fuel_flow_total');
            }
            else {
                bus.getSubscriber().on('event_bus_topic_first_sub').handle(this.onTopicSubscribed.bind(this));
            }
        }
        /**
         * Responds to when a topic is first subscribed to on the event bus.
         * @param topic The subscribed topic.
         */
        onTopicSubscribed(topic) {
            if (topic === 'fuel_flow_total') {
                this.needPublishTotalFuelFlow = true;
                for (let i = 1; i <= this.engineCount; i++) {
                    this.totalFuelFlowSimVarIds.push(SimVar.GetRegisteredId(`ENG FUEL FLOW GPH:${i}`, exports.SimVarValueType.GPH, ''));
                }
                if (this.publishActive) {
                    this.publishTotalFuelFlow();
                }
            }
        }
        /** @inheritDoc */
        startPublish() {
            super.startPublish();
            this.simVarPublisher.startPublish();
        }
        /** @inheritDoc */
        stopPublish() {
            super.stopPublish();
            this.simVarPublisher.stopPublish();
        }
        /** @inheritDoc */
        onUpdate() {
            this.simVarPublisher.onUpdate();
            if (this.needPublishTotalFuelFlow) {
                this.publishTotalFuelFlow();
            }
        }
        /**
         * Publishes the `fuel_flow_total` topic.
         */
        publishTotalFuelFlow() {
            let totalFuelFlow = 0;
            for (let i = 0; i < this.totalFuelFlowSimVarIds.length; i++) {
                totalFuelFlow += SimVar.GetSimVarValueFastReg(this.totalFuelFlowSimVarIds[i]);
            }
            this.publish('fuel_flow_total', totalFuelFlow);
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A publisher for Engine information.
     */
    class WeightBalanceSimvarPublisher extends SimVarPublisher {
        /**
         * Create a WeightAndBalancePublisher.
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer) {
            const simvars = new Map([
                ['cg_percent', { name: 'CG PERCENT', type: exports.SimVarValueType.Percent }],
                ['total_weight', { name: 'TOTAL WEIGHT', type: exports.SimVarValueType.Pounds }],
                ['payload_station_weight', { name: 'PAYLOAD STATION WEIGHT:#index#', type: exports.SimVarValueType.Pounds, indexed: true }],
            ]);
            super(simvars, bus, pacer);
        }
        /** @inheritdoc */
        onUpdate() {
            super.onUpdate();
        }
    }

    /**
     * Flight timer modes.
     */
    exports.FlightTimerMode = void 0;
    (function (FlightTimerMode) {
        FlightTimerMode[FlightTimerMode["CountingDown"] = 0] = "CountingDown";
        FlightTimerMode[FlightTimerMode["CountingUp"] = 1] = "CountingUp";
    })(exports.FlightTimerMode || (exports.FlightTimerMode = {}));
    /**
     * A utility class for working with flight timers.
     */
    class FlightTimerUtils {
        /**
         * Gets the event bus topic suffix for a flight timer ID.
         * @param id The ID for which to get the suffix.
         * @returns The event bus topic suffix for the specified flight timer ID.
         */
        static getIdSuffix(id) {
            return (id === '' ? '' : `_${id}`);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static onEvent(id, index, arg2, baseTopic) {
            return (arg2 instanceof EventBus ? arg2.getSubscriber() : arg2).on(`${baseTopic}${FlightTimerUtils.getIdSuffix(id)}_${index}`);
        }
    }
    /**
     * A controller for a set of flight timers.
     */
    class FlightTimerController {
        /**
         * Creates a new instance of FlightTimerController.
         * @param bus The event bus.
         * @param id The ID of the timers controlled by this controller.
         */
        constructor(bus, id) {
            this.bus = bus;
            this.id = id;
            this.publisher = this.bus.getPublisher();
            const suffix = FlightTimerUtils.getIdSuffix(id);
            this.topicMap = {
                'timer_set_mode': `timer_set_mode${suffix}`,
                'timer_set_initial_value': `timer_set_initial_value${suffix}`,
                'timer_set_value': `timer_set_value${suffix}`,
                'timer_start': `timer_start${suffix}`,
                'timer_stop': `timer_stop${suffix}`,
                'timer_reset': `timer_reset${suffix}`
            };
        }
        /**
         * Sets the active counting mode for a timer.
         * @param index The index of the timer.
         * @param mode The mode to set.
         */
        setMode(index, mode) {
            this.publisher.pub(`${this.topicMap['timer_set_mode']}_${index}`, mode, true, false);
        }
        /**
         * Sets the initial value for a timer.
         * @param index The index of the timer.
         * @param initialValue The initial value to set, in milliseconds.
         */
        setInitialValue(index, initialValue) {
            this.publisher.pub(`${this.topicMap['timer_set_initial_value']}_${index}`, initialValue, true, false);
        }
        /**
         * Sets the current value for a timer.
         * @param index The index of the timer.
         * @param value The value to set, in milliseconds.
         */
        setValue(index, value) {
            this.publisher.pub(`${this.topicMap['timer_set_value']}_${index}`, value, true, false);
        }
        /**
         * Starts a timer.
         * @param index The index of the timer.
         */
        start(index) {
            this.publisher.pub(`${this.topicMap['timer_start']}_${index}`, undefined, true, false);
        }
        /**
         * Stops a timer.
         * @param index The index of the timer.
         */
        stop(index) {
            this.publisher.pub(`${this.topicMap['timer_stop']}_${index}`, undefined, true, false);
        }
        /**
         * Resets a timer to its initial value.
         * @param index The index of the timer.
         */
        reset(index) {
            this.publisher.pub(`${this.topicMap['timer_reset']}_${index}`, undefined, true, false);
        }
    }
    /**
     * A publisher for flight timer information.
     */
    class FlightTimerPublisher extends SimVarPublisher {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(bus, arg2, pacer) {
            var _a;
            const id = (typeof arg2 === 'number' ? '' : ((_a = arg2 === null || arg2 === void 0 ? void 0 : arg2.id) !== null && _a !== void 0 ? _a : ''));
            const suffix = FlightTimerUtils.getIdSuffix(id);
            const entries = [
                [`timer_mode${suffix}`, { name: `L:WTFltTimer_Mode${suffix}:#index#`, type: exports.SimVarValueType.Number, indexed: true, defaultIndex: null }],
                [`timer_is_running${suffix}`, { name: `L:WTFltTimer_Running${suffix}:#index#`, type: exports.SimVarValueType.Bool, indexed: true, defaultIndex: null }],
                [`timer_initial_value_ms${suffix}`, { name: `L:WTFltTimer_Initial_Value${suffix}:#index#`, type: exports.SimVarValueType.Number, indexed: true, defaultIndex: null }],
                [`timer_value_ms${suffix}`, { name: `L:WTFltTimer_Value${suffix}:#index#`, type: exports.SimVarValueType.Number, indexed: true, defaultIndex: null }]
            ];
            super(entries, bus, pacer);
        }
    }
    /**
     * An instrument which manages zero or more flight timers. Requires the topics defined in {@link ClockEvents} to be
     * actively published to the event bus in order to function properly. All timers operate using simulation time and are
     * updated every instrument update cycle with up to millisecond precision.
     */
    class FlightTimerInstrument {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(bus, arg2) {
            var _a;
            this.bus = bus;
            this.timers = {};
            this.simTime = ConsumerSubject.create(this.bus.getSubscriber().on('simTime'), 0).pause();
            this.lastUpdateSimTime = this.simTime.get();
            if (typeof arg2 === 'number') {
                this.id = '';
                this.timerCount = Math.max(arg2, 0);
            }
            else {
                this.id = (_a = arg2.id) !== null && _a !== void 0 ? _a : '';
                this.timerCount = Math.max(arg2.count, 0);
            }
            for (let i = 1; i <= this.timerCount; i++) {
                this.timers[i] = new FlightTimer(bus, this.id, i);
            }
        }
        /** @inheritDoc */
        init() {
            this.simTime.resume();
            this.lastUpdateSimTime = this.simTime.get();
            for (let i = 1; i <= this.timerCount; i++) {
                this.timers[i].init(this.lastUpdateSimTime);
            }
        }
        /** @inheritDoc */
        onUpdate() {
            const currentSimTime = this.simTime.get();
            for (let i = 1; i <= this.timerCount; i++) {
                this.timers[i].update(currentSimTime);
            }
            this.lastUpdateSimTime = currentSimTime;
        }
    }
    /**
     * A flight timer.
     */
    class FlightTimer {
        /**
         * Creates a new instance of FlightTimer.
         * @param bus The event bus.
         * @param id The ID of this timer.
         * @param index The index of this timer.
         */
        constructor(bus, id, index) {
            this.bus = bus;
            this.id = id;
            this.index = index;
            this.idSuffix = FlightTimerUtils.getIdSuffix(this.id);
            this.simVars = {
                mode: `L:WTFltTimer_Mode${this.idSuffix}:${this.index}`,
                isRunning: `L:WTFltTimer_Running${this.idSuffix}:${this.index}`,
                referenceTime: `L:WTFltTimer_Reference_Time${this.idSuffix}:${this.index}`,
                referenceValue: `L:WTFltTimer_Reference_Value${this.idSuffix}:${this.index}`,
                initialValue: `L:WTFltTimer_Initial_Value${this.idSuffix}:${this.index}`,
                currentValue: `L:WTFltTimer_Value${this.idSuffix}:${this.index}`
            };
            this.simTime = 0;
            this.mode = exports.FlightTimerMode.CountingDown;
            this.isRunning = false;
            this.referenceTime = 0;
            this.referenceValue = 0;
            this.initialValue = 0;
            this.currentValue = 0;
        }
        /**
         * Initializes this timer. Once this timer is initialized, it will respond to timer control events.
         * @param time The current sim time, as a UNIX timestamp in milliseconds.
         */
        init(time) {
            this.simTime = time;
            // Initialize state from SimVars
            this.mode = SimVar.GetSimVarValue(this.simVars.mode, exports.SimVarValueType.Number);
            this.isRunning = !!SimVar.GetSimVarValue(this.simVars.isRunning, exports.SimVarValueType.Bool);
            this.referenceTime = SimVar.GetSimVarValue(this.simVars.referenceTime, exports.SimVarValueType.Number);
            this.referenceValue = SimVar.GetSimVarValue(this.simVars.referenceValue, exports.SimVarValueType.Number);
            this.initialValue = SimVar.GetSimVarValue(this.simVars.initialValue, exports.SimVarValueType.Number);
            this.currentValue = SimVar.GetSimVarValue(this.simVars.currentValue, exports.SimVarValueType.Number);
            // Subscribe to control events
            const sub = this.bus.getSubscriber();
            sub.on(`timer_set_mode${this.idSuffix}_${this.index}`).handle(mode => {
                this.mode = mode;
                this.referenceTime = this.simTime;
                this.referenceValue = this.currentValue;
                SimVar.SetSimVarValue(this.simVars.mode, exports.SimVarValueType.Number, mode);
                SimVar.SetSimVarValue(this.simVars.referenceTime, exports.SimVarValueType.Number, this.referenceTime);
                SimVar.SetSimVarValue(this.simVars.referenceValue, exports.SimVarValueType.Number, this.referenceValue);
            });
            sub.on(`timer_set_initial_value${this.idSuffix}_${this.index}`).handle(value => {
                this.initialValue = value;
                SimVar.SetSimVarValue(this.simVars.initialValue, exports.SimVarValueType.Number, this.initialValue);
            });
            sub.on(`timer_set_value${this.idSuffix}_${this.index}`).handle(value => {
                this.referenceTime = this.simTime;
                this.referenceValue = value;
                this.currentValue = value;
                SimVar.SetSimVarValue(this.simVars.referenceTime, exports.SimVarValueType.Number, this.referenceTime);
                SimVar.SetSimVarValue(this.simVars.referenceValue, exports.SimVarValueType.Number, this.referenceValue);
            });
            sub.on(`timer_start${this.idSuffix}_${this.index}`).handle(() => {
                if (this.isRunning) {
                    return;
                }
                this.isRunning = true;
                this.referenceTime = this.simTime;
                this.referenceValue = this.currentValue;
                SimVar.SetSimVarValue(this.simVars.referenceTime, exports.SimVarValueType.Number, this.referenceTime);
                SimVar.SetSimVarValue(this.simVars.referenceValue, exports.SimVarValueType.Number, this.referenceValue);
                SimVar.SetSimVarValue(this.simVars.isRunning, exports.SimVarValueType.Bool, 1);
            });
            sub.on(`timer_stop${this.idSuffix}_${this.index}`).handle(() => {
                if (!this.isRunning) {
                    return;
                }
                this.isRunning = false;
                SimVar.SetSimVarValue(this.simVars.isRunning, exports.SimVarValueType.Bool, 0);
            });
            sub.on(`timer_reset${this.idSuffix}_${this.index}`).handle(() => {
                this.referenceTime = this.simTime;
                this.referenceValue = this.initialValue;
                this.currentValue = this.initialValue;
                SimVar.SetSimVarValue(this.simVars.referenceTime, exports.SimVarValueType.Number, this.referenceTime);
                SimVar.SetSimVarValue(this.simVars.referenceValue, exports.SimVarValueType.Number, this.referenceValue);
                SimVar.SetSimVarValue(this.simVars.currentValue, exports.SimVarValueType.Number, this.currentValue);
            });
        }
        /**
         * Updates this timer's current value.
         * @param time The current sim time, as a UNIX timestamp in milliseconds.
         */
        update(time) {
            this.simTime = time;
            if (!this.isRunning) {
                return;
            }
            const modeSign = this.mode === exports.FlightTimerMode.CountingDown ? -1 : 1;
            const value = this.referenceValue + (time - this.referenceTime) * modeSign;
            this.currentValue = value;
            SimVar.SetSimVarValue(this.simVars.currentValue, exports.SimVarValueType.Number, value);
        }
    }

    /**
     * Bit flags describing ground track topics published by {@link GNSSPublisher}.
     */
    var TrackPublishFlag;
    (function (TrackPublishFlag) {
        TrackPublishFlag[TrackPublishFlag["None"] = 0] = "None";
        TrackPublishFlag[TrackPublishFlag["Gated"] = 1] = "Gated";
        TrackPublishFlag[TrackPublishFlag["Raw"] = 2] = "Raw";
        TrackPublishFlag[TrackPublishFlag["True"] = 4] = "True";
        TrackPublishFlag[TrackPublishFlag["Magnetic"] = 8] = "Magnetic";
    })(TrackPublishFlag || (TrackPublishFlag = {}));
    /**
     * A publisher for global positioning and inertial data.
     */
    class GNSSPublisher extends BasePublisher {
        /**
         * Create an GNSSPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer = undefined) {
            super(bus, pacer);
            this.vec3Cache = [Vec3Math.create(), Vec3Math.create()];
            this.simVarPublisher = new SimVarPublisher(new Map([
                ['zulu_time', { name: 'E:ZULU TIME', type: exports.SimVarValueType.Seconds }],
                ['time_of_day', { name: 'E:TIME OF DAY', type: exports.SimVarValueType.Number }],
                ['ground_speed', { name: 'GROUND VELOCITY', type: exports.SimVarValueType.Knots }],
                ['ground_altitude', { name: 'GROUND ALTITUDE', type: exports.SimVarValueType.Feet }],
                ['above_ground_height', { name: 'PLANE ALT ABOVE GROUND', type: exports.SimVarValueType.Feet }],
                ['inertial_vertical_speed', { name: 'VELOCITY WORLD Y', type: exports.SimVarValueType.FPM }]
            ]), this.bus, this.pacer);
            this.registeredSimVarIds = {
                latitude: SimVar.GetRegisteredId('PLANE LATITUDE', exports.SimVarValueType.Degree, ''),
                longitude: SimVar.GetRegisteredId('PLANE LONGITUDE', exports.SimVarValueType.Degree, ''),
                altitude: SimVar.GetRegisteredId('PLANE ALTITUDE', exports.SimVarValueType.Meters, ''),
                magVar: SimVar.GetRegisteredId('MAGVAR', exports.SimVarValueType.Degree, ''),
                heading: SimVar.GetRegisteredId('PLANE HEADING DEGREES TRUE', exports.SimVarValueType.Degree, ''),
                velocityWorldX: SimVar.GetRegisteredId('VELOCITY WORLD X', exports.SimVarValueType.Knots, ''),
                velocityWorldZ: SimVar.GetRegisteredId('VELOCITY WORLD Z', exports.SimVarValueType.Knots, ''),
                velocityBodyX: SimVar.GetRegisteredId('VELOCITY BODY X', exports.SimVarValueType.MetersPerSecond, ''),
                velocityBodyY: SimVar.GetRegisteredId('VELOCITY BODY Y', exports.SimVarValueType.MetersPerSecond, ''),
                velocityBodyZ: SimVar.GetRegisteredId('VELOCITY BODY Z', exports.SimVarValueType.MetersPerSecond, ''),
                accelerationBodyX: SimVar.GetRegisteredId('ACCELERATION BODY X', exports.SimVarValueType.MetersPerSecond, ''),
                accelerationBodyY: SimVar.GetRegisteredId('ACCELERATION BODY Y', exports.SimVarValueType.MetersPerSecond, ''),
                accelerationBodyZ: SimVar.GetRegisteredId('ACCELERATION BODY Z', exports.SimVarValueType.MetersPerSecond, ''),
            };
            this.needPublish = {
                'gps-position': false,
                'magvar': false,
                'inertial_speed': false,
                'inertial_acceleration': false,
                'inertial_track_acceleration': false
            };
            this.trackTopicToPublishFlagsMap = {
                'track_deg_true': TrackPublishFlag.Gated | TrackPublishFlag.True,
                'track_deg_magnetic': TrackPublishFlag.Gated | TrackPublishFlag.Magnetic,
                'raw_track_deg_true': TrackPublishFlag.Raw | TrackPublishFlag.True,
                'raw_track_deg_magnetic': TrackPublishFlag.Raw | TrackPublishFlag.Magnetic
            };
            this.trackPublishFlags = TrackPublishFlag.None;
            this.prevGatedTrueTrack = undefined;
            this.prevGatedMagneticTrack = undefined;
            for (const topic in this.needPublish) {
                this.needPublish[topic] = bus.getTopicSubscriberCount(topic) > 0;
            }
            for (const topic in this.trackTopicToPublishFlagsMap) {
                if (bus.getTopicSubscriberCount(topic) > 0) {
                    this.trackPublishFlags |= this.trackTopicToPublishFlagsMap[topic];
                }
            }
            bus.getSubscriber().on('event_bus_topic_first_sub').handle(this.onTopicSubscribed.bind(this));
        }
        /**
         * Responds to when a topic is first subscribed to on the event bus.
         * @param topic The subscribed topic.
         */
        onTopicSubscribed(topic) {
            if (topic in this.needPublish) {
                this.needPublish[topic] = true;
                if (this.publishActive) {
                    switch (topic) {
                        case 'gps-position':
                            this.publishPosition();
                            break;
                        case 'magvar':
                            this.publishTrack(TrackPublishFlag.None, true);
                            break;
                        case 'inertial_speed':
                            this.publishInertialData(true, false, false);
                            break;
                        case 'inertial_acceleration':
                            this.publishInertialData(false, true, false);
                            break;
                        case 'inertial_track_acceleration':
                            this.publishInertialData(false, false, true);
                            break;
                    }
                }
            }
            else if (topic in this.trackTopicToPublishFlagsMap) {
                const topicTrackPublishFlags = this.trackTopicToPublishFlagsMap[topic];
                this.trackPublishFlags |= topicTrackPublishFlags;
                if (this.publishActive) {
                    this.publishTrack(topicTrackPublishFlags, false);
                }
            }
        }
        /** @inheritdoc */
        startPublish() {
            super.startPublish();
            this.simVarPublisher.startPublish();
        }
        /** @inheritdoc */
        stopPublish() {
            super.stopPublish();
            this.simVarPublisher.stopPublish();
        }
        /** @inheritdoc */
        onUpdate() {
            this.needPublish['gps-position'] && this.publishPosition();
            this.publishTrack(this.trackPublishFlags, this.needPublish['magvar']);
            this.publishInertialData(this.needPublish['inertial_speed'], this.needPublish['inertial_acceleration'], this.needPublish['inertial_track_acceleration']);
            this.simVarPublisher.onUpdate();
        }
        /**
         * Publishes the gps-position event.
         */
        publishPosition() {
            const lat = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.latitude);
            const lon = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.longitude);
            const alt = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.altitude);
            this.publish('gps-position', new LatLongAlt(lat, lon, alt));
        }
        /**
         * Publishes the `track_deg_true`, `raw_track_deg_true`, `track_deg_magnetic`, `raw_track_deg_magnetic`, and `magvar`
         * topics.
         * @param trackFlags Bit flags describing which of the track topics to publish.
         * @param publishMagvar Whether to publish the `magvar` topic.
         */
        publishTrack(trackFlags, publishMagvar) {
            var _a;
            const velocityEW = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.velocityWorldX);
            const velocityNS = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.velocityWorldZ);
            let trueTrackRaw = 0;
            if (velocityEW === 0 && velocityNS === 0) {
                trueTrackRaw = NaN;
            }
            else {
                trueTrackRaw = NavMath.normalizeHeading(Math.atan2(velocityEW, velocityNS) * Avionics.Utils.RAD2DEG);
            }
            const magVar = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.magVar);
            if (velocityEW * velocityEW + velocityNS * velocityNS > 0.01) {
                this.prevGatedTrueTrack = trueTrackRaw;
            }
            else {
                (_a = this.prevGatedTrueTrack) !== null && _a !== void 0 ? _a : (this.prevGatedTrueTrack = SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.heading));
            }
            this.prevGatedMagneticTrack = MagVar.trueToMagnetic(this.prevGatedTrueTrack, magVar);
            if (trackFlags === TrackPublishFlag.None) {
                return;
            }
            const publishGated = BitFlags.isAll(trackFlags, TrackPublishFlag.Gated);
            const publishRaw = BitFlags.isAll(trackFlags, TrackPublishFlag.Raw);
            const publishTrue = BitFlags.isAll(trackFlags, TrackPublishFlag.True);
            const publishMagnetic = BitFlags.isAll(trackFlags, TrackPublishFlag.Magnetic);
            if (publishRaw) {
                publishTrue && this.publish('raw_track_deg_true', trueTrackRaw);
                publishMagnetic && this.publish('raw_track_deg_magnetic', MagVar.trueToMagnetic(trueTrackRaw, magVar));
            }
            if (publishGated) {
                publishTrue && this.publish('track_deg_true', this.prevGatedTrueTrack);
                publishMagnetic && this.publish('track_deg_magnetic', this.prevGatedMagneticTrack);
            }
            publishMagvar && this.publish('magvar', magVar);
        }
        /**
         * Publishes the `inertial_speed`, `inertial_acceleration`, and `inertial_track_acceleration` topics.
         * @param publishSpeed Whether to publish the `inertial_speed` topic.
         * @param publishAcceleration Whether to publish the `inertial_acceleration` topic.
         * @param publishTrackAcceleration Whether to publish the `inertial_track_acceleration` topic.
         */
        publishInertialData(publishSpeed, publishAcceleration, publishTrackAcceleration) {
            const velocityVec = this.vec3Cache[0];
            const accelerationVec = this.vec3Cache[1];
            let speed = 0;
            let acceleration = 0;
            if (publishSpeed || publishTrackAcceleration) {
                Vec3Math.set(SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.velocityBodyX), SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.velocityBodyY), SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.velocityBodyZ), velocityVec);
                speed = Vec3Math.abs(velocityVec);
            }
            if (publishAcceleration || publishTrackAcceleration) {
                Vec3Math.set(SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.accelerationBodyX), SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.accelerationBodyY), SimVar.GetSimVarValueFastReg(this.registeredSimVarIds.accelerationBodyZ), accelerationVec);
                acceleration = Vec3Math.abs(accelerationVec);
            }
            publishSpeed && this.publish('inertial_speed', speed);
            publishAcceleration && this.publish('inertial_acceleration', acceleration);
            publishTrackAcceleration && this.publish('inertial_track_acceleration', speed === 0 ? acceleration : Vec3Math.dot(accelerationVec, velocityVec) / speed);
        }
        /**
         * Gets the airplane's instantaneous true ground track, in degrees.
         * @param defaultTrack The default value to return if the airplane's ground speed is less than the threshold. If not
         * defined, then the airplane's true heading (obtained from the `PLANE HEADING DEGREES TRUE` SimVar) will be used as
         * the default value.
         * @param groundSpeedThreshold The ground speed, in knots, at or below which the default value is returned instead of
         * the ground track.
         * @returns The airplane's instantaneous true ground track, in degrees, or the default value if the airplane's ground
         * speed is less than or equal to the specified threshold.
         */
        static getInstantaneousTrack(defaultTrack, groundSpeedThreshold = 0) {
            const velocityEW = SimVar.GetSimVarValue('VELOCITY WORLD X', exports.SimVarValueType.Knots);
            const velocityNS = SimVar.GetSimVarValue('VELOCITY WORLD Z', exports.SimVarValueType.Knots);
            if (velocityEW * velocityEW + velocityNS * velocityNS > Math.max(groundSpeedThreshold * groundSpeedThreshold, 0)) {
                return NavMath.normalizeHeading(Math.atan2(velocityEW, velocityNS) * Avionics.Utils.RAD2DEG);
            }
            else {
                return defaultTrack !== null && defaultTrack !== void 0 ? defaultTrack : SimVar.GetSimVarValue('PLANE HEADING DEGREES TRUE', exports.SimVarValueType.Degree);
            }
        }
    }

    /**
     * Types of changes made to {@link SubscribableSet}.
     */
    exports.SubscribableSetEventType = void 0;
    (function (SubscribableSetEventType) {
        /** A key was added. */
        SubscribableSetEventType["Added"] = "Added";
        /** A key was deleted. */
        SubscribableSetEventType["Deleted"] = "Deleted";
    })(exports.SubscribableSetEventType || (exports.SubscribableSetEventType = {}));

    /**
     * A pipe from an input subscribable set to an output mutable subscribable set. Each key added/removed notification
     * received by the pipe is used to add/remove keys to/from the output set.
     */
    class SubscribableSetPipe extends HandlerSubscription {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(from, to, arg3, arg4) {
            let handler;
            let initialNotifyFunc;
            let onDestroy;
            if (typeof arg4 === 'function') {
                const toCast = to;
                const map = arg3;
                handler = (set, type, key) => {
                    if (type === exports.SubscribableSetEventType.Added) {
                        toCast.add(map(key));
                    }
                    else {
                        const mappedKey = map(key);
                        // Only delete the mapped key if no other key in the input set maps to the same key
                        for (const inputKey of set) {
                            if (map(inputKey) === mappedKey) {
                                return;
                            }
                        }
                        toCast.delete(mappedKey);
                    }
                };
                initialNotifyFunc = () => {
                    const fromSet = from.get();
                    const toAdd = new Set();
                    for (const key of fromSet) {
                        toAdd.add(map(key));
                    }
                    for (const key of toCast.get()) {
                        if (!toAdd.delete(key)) {
                            toCast.delete(key);
                        }
                    }
                    for (const key of toAdd) {
                        toCast.add(key);
                    }
                };
                onDestroy = arg4;
            }
            else {
                const toCast = to;
                handler = (set, type, key) => {
                    if (type === exports.SubscribableSetEventType.Added) {
                        toCast.add(key);
                    }
                    else {
                        toCast.delete(key);
                    }
                };
                initialNotifyFunc = () => {
                    const fromSet = from.get();
                    const toAdd = new Set(fromSet);
                    for (const key of to.get()) {
                        if (!toAdd.delete(key)) {
                            toCast.delete(key);
                        }
                    }
                    for (const key of toAdd) {
                        toCast.add(key);
                    }
                };
                onDestroy = arg3;
            }
            super(handler, initialNotifyFunc, onDestroy);
        }
    }

    /**
     * An abstract implementation of a subscribable set which allows adding, removing, and notifying subscribers.
     */
    class AbstractSubscribableSet {
        constructor() {
            this.isSubscribable = true;
            this.isSubscribableSet = true;
            this.notifyDepth = 0;
            /** A function which sends initial notifications to subscriptions. */
            this.initialNotifyFunc = this.initialNotify.bind(this);
            /** A function which responds to when a subscription to this subscribable is destroyed. */
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /** @inheritdoc */
        get size() {
            return this.get().size;
        }
        /**
         * Adds a subscription to this set.
         * @param sub The subscription to add.
         */
        addSubscription(sub) {
            if (this.subs) {
                this.subs.push(sub);
            }
            else if (this.singletonSub) {
                this.subs = [this.singletonSub, sub];
                delete this.singletonSub;
            }
            else {
                this.singletonSub = sub;
            }
        }
        /** @inheritdoc */
        has(key) {
            return this.get().has(key);
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Notifies subscriptions of a change in this set.
         * @param type The type of change.
         * @param key The key related to the change.
         */
        notify(type, key) {
            const set = this.get();
            const canCleanUpSubs = this.notifyDepth === 0;
            let needCleanUpSubs = false;
            this.notifyDepth++;
            if (this.singletonSub) {
                try {
                    if (this.singletonSub.isAlive && !this.singletonSub.isPaused) {
                        this.singletonSub.handler(set, type, key);
                    }
                }
                catch (error) {
                    console.error(`AbstractSubscribableSet: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
                if (canCleanUpSubs) {
                    // If subscriptions were added during the notification, then singletonSub would be deleted and replaced with
                    // the subs array.
                    if (this.singletonSub) {
                        needCleanUpSubs = !this.singletonSub.isAlive;
                    }
                    else if (this.subs) {
                        for (let i = 0; i < this.subs.length; i++) {
                            if (!this.subs[i].isAlive) {
                                needCleanUpSubs = true;
                                break;
                            }
                        }
                    }
                }
            }
            else if (this.subs) {
                const subLen = this.subs.length;
                for (let i = 0; i < subLen; i++) {
                    try {
                        const sub = this.subs[i];
                        if (!sub.isPaused) {
                            sub.handler(set, type, key);
                        }
                        needCleanUpSubs || (needCleanUpSubs = canCleanUpSubs && !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`AbstractSubscribableSet: error in handler: ${error}`);
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                // If subscriptions were added during the notification and a cleanup operation is not already pending, then we
                // need to check if any of the new subscriptions are already dead and if so, pend a cleanup operation.
                if (canCleanUpSubs && !needCleanUpSubs) {
                    for (let i = subLen; i < this.subs.length; i++) {
                        if (!this.subs[i].isAlive) {
                            needCleanUpSubs = true;
                            break;
                        }
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs) {
                if (this.singletonSub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    this.subs = this.subs.filter(sub => sub.isAlive);
                }
            }
        }
        /**
         * Notifies a subscription of this set's current state.
         * @param sub The subscription to notify.
         */
        initialNotify(sub) {
            const set = this.get();
            for (const key of set) {
                sub.handler(set, exports.SubscribableSetEventType.Added, key);
            }
        }
        /**
         * Responds to when a subscription to this set is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                if (this.singletonSub === sub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    const index = this.subs.indexOf(sub);
                    if (index >= 0) {
                        this.subs.splice(index, 1);
                    }
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        map(fn, equalityFunc, mutateFunc, initialVal) {
            const mapFunc = (inputs, previousVal) => fn(inputs[0], previousVal);
            return mutateFunc
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                ? MappedSubject.create(mapFunc, equalityFunc, mutateFunc, initialVal, this)
                : MappedSubject.create(mapFunc, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, this);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        pipe(to, arg2, arg3) {
            let sub;
            let paused;
            if (typeof arg2 === 'function') {
                if ('isSubscribableSet' in to) {
                    sub = new SubscribableSetPipe(this, to, arg2, this.onSubDestroyedFunc);
                }
                else {
                    sub = new SubscribablePipe(this, to, arg2, this.onSubDestroyedFunc);
                }
                paused = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                if ('isSubscribableSet' in to) {
                    sub = new SubscribableSetPipe(this, to, this.onSubDestroyedFunc);
                }
                else {
                    sub = new SubscribablePipe(this, to, this.onSubDestroyedFunc);
                }
                paused = arg2 !== null && arg2 !== void 0 ? arg2 : false;
            }
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
    }

    /**
     * A subscribable set whose keys can be freely added and removed.
     */
    class SetSubject extends AbstractSubscribableSet {
        /**
         * Constructor.
         * @param initialKeys The keys initially contained in the new set. If not defined, then the new set will be
         * initialized to the empty set.
         */
        constructor(initialKeys) {
            super();
            this.isMutableSubscribable = true;
            this.isMutableSubscribableSet = true;
            this.backingSet = new Set(initialKeys);
        }
        /**
         * Creates and returns a new SetSubject.
         * @param initialKeys The keys initially contained in the new set. If not defined, then the new set will be
         * initialized to the empty set.
         * @returns A new SetSubject instance.
         */
        static create(initialKeys) {
            return new SetSubject(initialKeys);
        }
        /** @inheritdoc */
        get() {
            return this.backingSet;
        }
        /**
         * Sets the keys contained in this set.
         * @param keys The keys to set.
         */
        set(keys) {
            const toAdd = new Set(keys);
            for (const key of this.backingSet) {
                if (!toAdd.delete(key)) {
                    this.delete(key);
                }
            }
            for (const key of toAdd) {
                this.add(key);
            }
        }
        /** @inheritdoc */
        add(key) {
            const oldSize = this.backingSet.size;
            this.backingSet.add(key);
            if (oldSize !== this.backingSet.size) {
                this.notify(exports.SubscribableSetEventType.Added, key);
            }
            return this;
        }
        /** @inheritdoc */
        delete(key) {
            const wasDeleted = this.backingSet.delete(key);
            if (wasDeleted) {
                this.notify(exports.SubscribableSetEventType.Deleted, key);
            }
            return wasDeleted;
        }
        /**
         * Toggles the presence of a key in this set.
         * @param key The key to toggle.
         * @param force The state of the key to force. If `true`, the key will be added to this set. If `false`, the key will
         * be removed from this set. If not defined, the key will be added to this set if it is not already present and
         * removed if it is already present.
         * @returns Whether the key is present in this set after the toggle operation.
         */
        toggle(key, force) {
            const shouldAdd = force !== null && force !== void 0 ? force : !this.backingSet.has(key);
            if (shouldAdd) {
                this.add(key);
            }
            else {
                this.delete(key);
            }
            // Explicitly query the set again instead of just returning shouldAdd in case the key was manipulated in a handler
            // triggered by its addition/removal
            return this.backingSet.has(key);
        }
        /**
         * Removes all keys from this set.
         */
        clear() {
            for (const key of this.backingSet) {
                this.backingSet.delete(key);
                this.notify(exports.SubscribableSetEventType.Deleted, key);
            }
        }
    }

    /**
     * An accessible value whose state can be freely manipulated.
     */
    class Value {
        /**
         * Creates a new instance of Value.
         * @param initialValue The initial value of the Value.
         */
        constructor(initialValue) {
            this.value = initialValue;
        }
        /**
         * Creates and returns a new Value.
         * @param initialValue The initial value of the Value.
         * @returns A new Value with the specified initial value.
         */
        static create(initialValue) {
            return new Value(initialValue);
        }
        /** @inheritDoc */
        get() {
            return this.value;
        }
        /** @inheritDoc */
        set(value) {
            this.value = value;
        }
    }

    /**
     * SBAS group names.
     */
    exports.SBASGroupName = void 0;
    (function (SBASGroupName) {
        /** Wide Area Augmentation System (USA). */
        SBASGroupName["WAAS"] = "WAAS";
        /** European Geostationary Navigation Overlay Service (EU). */
        SBASGroupName["EGNOS"] = "EGNOS";
        /** GPS Aided Geo Augmented Navigation System (India). */
        SBASGroupName["GAGAN"] = "GAGAN";
        /** Multi-functional Satellite Augmentation System (Japan). */
        SBASGroupName["MSAS"] = "MSAS";
    })(exports.SBASGroupName || (exports.SBASGroupName = {}));
    /**
     * Possible state on GPS satellites.
     */
    exports.GPSSatelliteState = void 0;
    (function (GPSSatelliteState) {
        /** There is no current valid state. */
        GPSSatelliteState[GPSSatelliteState["None"] = 0] = "None";
        /** The satellite is out of view and cannot be reached. */
        GPSSatelliteState[GPSSatelliteState["Unreachable"] = 1] = "Unreachable";
        /** The satellite has been found and data is being downloaded. */
        GPSSatelliteState[GPSSatelliteState["Acquired"] = 2] = "Acquired";
        /** The satellite is faulty. */
        GPSSatelliteState[GPSSatelliteState["Faulty"] = 3] = "Faulty";
        /** The satellite has been found, data is downloaded, but is not presently used in the GPS solution. */
        GPSSatelliteState[GPSSatelliteState["DataCollected"] = 4] = "DataCollected";
        /** The satellite is being active used in the GPS solution. */
        GPSSatelliteState[GPSSatelliteState["InUse"] = 5] = "InUse";
        /** The satellite is being active used in the GPS solution and SBAS differential corrections are being applied. */
        GPSSatelliteState[GPSSatelliteState["InUseDiffApplied"] = 6] = "InUseDiffApplied";
    })(exports.GPSSatelliteState || (exports.GPSSatelliteState = {}));
    /**
     * Possible {@link GPSSatComputer} states.
     */
    exports.GPSSystemState = void 0;
    (function (GPSSystemState) {
        /** The GPS receiver is searching for any visible satellites to acquire. */
        GPSSystemState["Searching"] = "Searching";
        /** The GPS receiver is in the process of acquiring satellites. */
        GPSSystemState["Acquiring"] = "Acquiring";
        /** A 3D solution has been acquired. */
        GPSSystemState["SolutionAcquired"] = "SolutionAcquired";
        /** A 3D solution using differential computations has been acquired. */
        GPSSystemState["DiffSolutionAcquired"] = "DiffSolutionAcquired";
    })(exports.GPSSystemState || (exports.GPSSystemState = {}));
    /**
     * Possible SBAS connection states.
     */
    exports.GPSSystemSBASState = void 0;
    (function (GPSSystemSBASState) {
        /** SBAS is disabled. */
        GPSSystemSBASState["Disabled"] = "Disabled";
        /** SBAS is enabled but not receiving differential corrections. */
        GPSSystemSBASState["Inactive"] = "Inactive";
        /** SBAS is enabled and is receiving differential corrections. */
        GPSSystemSBASState["Active"] = "Active";
    })(exports.GPSSystemSBASState || (exports.GPSSystemSBASState = {}));
    /**
     * An instrument that computes GPS satellite information.
     */
    class GPSSatComputer {
        /**
         * Gets the current satellites that are being tracked by this computer.
         * @returns The collection of current satellites.
         */
        get sats() {
            return this.satellites;
        }
        /**
         * Gets the current GPS system state.
         * @returns The current GPS system state.
         */
        get state() {
            return this._state;
        }
        /**
         * Gets the current GPS system SBAS state.
         * @returns The current GPS system SBAS state.
         */
        get sbasState() {
            return this._sbasState;
        }
        /**
         * Gets this system's current position dilution of precision value (PDOP), or `-1` if this system has not acquired a
         * position solution.
         * @returns This system's current position dilution of precision value (PDOP), or `-1` if this system has not
         * acquired a position solution.
         */
        get pdop() {
            return this._pdop;
        }
        /**
         * Gets this system's current horizontal dilution of precision value (HDOP), or `-1` if this system has not acquired a
         * position solution.
         * @returns This system's current horizontal dilution of precision value (HDOP), or `-1` if this system has not
         * acquired a position solution.
         */
        get hdop() {
            return this._hdop;
        }
        /**
         * Gets this system's current vertical dilution of precision value (VDOP), or `-1` if this system has not acquired a
         * position solution.
         * @returns This system's current vertical dilution of precision value (VDOP), or `-1` if this system has not
         * acquired a position solution.
         */
        get vdop() {
            return this._vdop;
        }
        /**
         * Creates an instance of GPSSatComputer.
         * @param index The index of this computer.
         * @param bus An instance of the event bus.
         * @param ephemerisFile The HTTP path to the ephemeris file to use for computations.
         * @param sbasFile The HTTP path to the SBAS definitions file.
         * @param updateInterval The interval in milliseconds to update the satellite positions.
         * @param enabledSBASGroups The names of the SBAS satellite groups for which signal reception is enabled. If the
         * computer's sync role is `replica`, then this parameter is ignored and the computer will sync enabled SBAS groups
         * from the primary instance.
         * @param syncRole This computer's sync role. A `primary` computer will broadcast sync events through the event bus
         * that allow corresponding `replica` computers to sync their state with the primary. A computer with a sync role of
         * `none` neither broadcasts sync events nor receives them; it maintains its own independent state. Defaults to
         * `none`.
         * @param options Options with which to configure the computer.
         */
        constructor(index, bus, ephemerisFile, sbasFile, updateInterval, enabledSBASGroups, syncRole = 'none', options) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
            var _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7;
            this.index = index;
            this.bus = bus;
            this.ephemerisFile = ephemerisFile;
            this.sbasFile = sbasFile;
            this.updateInterval = updateInterval;
            this.syncRole = syncRole;
            this.publisher = this.bus.getPublisher();
            this.syncPublisher = this.bus.getPublisher();
            this.nominalChannelCountTopic = `gps_system_nominal_channel_count_${this.index}`;
            this.stateChangedTopic = `gps_system_state_changed_${this.index}`;
            this.satStateChangedTopic = `gps_sat_state_changed_${this.index}`;
            this.satPosCalcTopic = `gps_sat_pos_calculated_${this.index}`;
            this.sbasStateChangedTopic = `gps_system_sbas_state_changed_${this.index}`;
            this.pdopTopic = `gps_system_pdop_${this.index}`;
            this.hdopTopic = `gps_system_hdop_${this.index}`;
            this.vdopTopic = `gps_system_vdop_${this.index}`;
            this.channelStateSyncTopic = `gps_system_sync_channel_state_changed_${this.index}`;
            this.satCalcSyncTopic = `gps_system_sync_sat_calc_${this.index}`;
            this.satStateSyncTopic = `gps_system_sync_sat_state_changed_${this.index}`;
            this.enabledSbasSyncTopic = `gps_system_sync_enabled_sbas_changed_${this.index}`;
            this.resetSyncTopic = `gps_system_sync_reset_${this.index}`;
            this.stateRequestSyncTopic = `gps_system_sync_state_request_${this.index}`;
            this.stateResponseSyncTopic = `gps_system_sync_state_response_${this.index}`;
            this.ephemerisData = {};
            this.sbasData = [];
            this.sbasServiceAreas = new Map();
            this.currentAvailableSbasGroups = new Set();
            this.satellites = [];
            this.publishedSatStates = [];
            this.channels = [];
            this.ppos = new GeoPoint(0, 0);
            this.pposVec = new Float64Array(2);
            this.lastKnownPosition = new GeoPoint(NaN, NaN);
            this.distanceFromLastKnownPos = 0;
            this.altitude = 0;
            this.simTime = 0;
            this.previousSimTime = 0;
            this.lastUpdateTime = undefined;
            this._state = exports.GPSSystemState.Searching;
            this._sbasState = exports.GPSSystemSBASState.Disabled;
            this.dops = Vec3Math.create();
            this._pdop = -1;
            this._hdop = -1;
            this._vdop = -1;
            this.isInit = false;
            this.needAcquireAndUse = false;
            this.needSatCalc = false;
            this.pendingChannelStateUpdates = new Map();
            this.pendingSatStateUpdates = new Map();
            this.almanacProgress = 0;
            this.lastAlamanacTime = undefined;
            this._isAlmanacValid = false;
            this.covarMatrix = [
                new Float64Array(4),
                new Float64Array(4),
                new Float64Array(4),
                new Float64Array(4),
            ];
            this.ephemerisCollectedSatelliteFilter = (sat) => {
                return GPSSatComputer.EPHEMERIS_COLLECTED_SATELLITE_STATES.has(sat.state.get());
            };
            this.losSatelliteFilter = (sat) => {
                return sat.signalStrength.get() > 0.05
                    && ((this.distanceFromLastKnownPos < 0.0290367 // 100 nautical miles
                        && (this._isAlmanacValid || sat.isCachedEphemerisValid(this.simTime)))
                        || GPSSatComputer.EPHEMERIS_COLLECTED_SATELLITE_STATES.has(sat.state.get()));
            };
            this.losSatelliteFilterOmniscient = (sat) => {
                return sat.signalStrength.get() > 0.05;
            };
            this.untrackedSatelliteFilter = (sat) => {
                return !this.channels.includes(sat) && sat.state.get() !== exports.GPSSatelliteState.Unreachable;
            };
            this.satelliteCosts = [];
            this.satelliteCostCompare = (indexA, indexB) => {
                return this.satelliteCosts[indexA] - this.satelliteCosts[indexB];
            };
            this.collectingDataSatelliteFilter = (sat) => {
                return sat !== null && GPSSatComputer.COLLECTING_DATA_SATELLITE_STATES.has(sat.state.get());
            };
            const desiredChannelCount = Math.max((_a = options === null || options === void 0 ? void 0 : options.channelCount) !== null && _a !== void 0 ? _a : Infinity, 4);
            this.nominalChannelCount = isFinite(desiredChannelCount) ? desiredChannelCount : null;
            this._channelCount = desiredChannelCount;
            this.satInUseMaxCount = SubscribableUtils.toSubscribable((_b = options === null || options === void 0 ? void 0 : options.satInUseMaxCount) !== null && _b !== void 0 ? _b : Infinity, true);
            this.satInUsePdopTarget = SubscribableUtils.toSubscribable((_c = options === null || options === void 0 ? void 0 : options.satInUsePdopTarget) !== null && _c !== void 0 ? _c : -1, true);
            this.satInUseOptimumCount = SubscribableUtils.toSubscribable((_d = options === null || options === void 0 ? void 0 : options.satInUseOptimumCount) !== null && _d !== void 0 ? _d : 4, true);
            this.satelliteTimingOptions = Object.assign({}, options === null || options === void 0 ? void 0 : options.timingOptions);
            (_e = (_u = this.satelliteTimingOptions).almanacExpireTime) !== null && _e !== void 0 ? _e : (_u.almanacExpireTime = 7776000000);
            (_f = (_v = this.satelliteTimingOptions).ephemerisExpireTime) !== null && _f !== void 0 ? _f : (_v.ephemerisExpireTime = 7200000);
            (_g = (_w = this.satelliteTimingOptions).acquisitionTimeout) !== null && _g !== void 0 ? _g : (_w.acquisitionTimeout = 30000);
            (_h = (_x = this.satelliteTimingOptions).acquisitionTime) !== null && _h !== void 0 ? _h : (_x.acquisitionTime = 30000);
            (_j = (_y = this.satelliteTimingOptions).acquisitionTimeRange) !== null && _j !== void 0 ? _j : (_y.acquisitionTimeRange = 15000);
            (_k = (_z = this.satelliteTimingOptions).acquisitionTimeWithEphemeris) !== null && _k !== void 0 ? _k : (_z.acquisitionTimeWithEphemeris = 15000);
            (_l = (_0 = this.satelliteTimingOptions).acquisitionTimeRangeWithEphemeris) !== null && _l !== void 0 ? _l : (_0.acquisitionTimeRangeWithEphemeris = 5000);
            (_m = (_1 = this.satelliteTimingOptions).unreachableExpireTime) !== null && _m !== void 0 ? _m : (_1.unreachableExpireTime = 3600000);
            (_o = (_2 = this.satelliteTimingOptions).ephemerisDownloadTime) !== null && _o !== void 0 ? _o : (_2.ephemerisDownloadTime = 30000);
            (_p = (_3 = this.satelliteTimingOptions).almanacDownloadTime) !== null && _p !== void 0 ? _p : (_3.almanacDownloadTime = 750000);
            (_q = (_4 = this.satelliteTimingOptions).sbasEphemerisDownloadTime) !== null && _q !== void 0 ? _q : (_4.sbasEphemerisDownloadTime = 60500);
            (_r = (_5 = this.satelliteTimingOptions).sbasEphemerisDownloadTimeRange) !== null && _r !== void 0 ? _r : (_5.sbasEphemerisDownloadTimeRange = 59500);
            (_s = (_6 = this.satelliteTimingOptions).sbasCorrectionDownloadTime) !== null && _s !== void 0 ? _s : (_6.sbasCorrectionDownloadTime = 150500);
            (_t = (_7 = this.satelliteTimingOptions).sbasCorrectionDownloadTimeRange) !== null && _t !== void 0 ? _t : (_7.sbasCorrectionDownloadTimeRange = 149500);
            if (syncRole === 'replica') {
                this.enabledSBASGroups = Value.create(this.enabledSBASGroupsSet = new Set());
            }
            else {
                this.enabledSBASGroups = enabledSBASGroups !== undefined && 'isSubscribableSet' in enabledSBASGroups
                    ? enabledSBASGroups
                    : SetSubject.create(enabledSBASGroups);
            }
            // Initialize these properties directly from SimVars in case the computer is created before values are published
            // to the bus.
            this.ppos.set(SimVar.GetSimVarValue('PLANE LATITUDE', exports.SimVarValueType.Degree), SimVar.GetSimVarValue('PLANE LONGITUDE', exports.SimVarValueType.Degree));
            this.altitude = SimVar.GetSimVarValue('PLANE ALTITUDE', exports.SimVarValueType.Meters);
            this.simTime = (SimVar.GetSimVarValue('E:ABSOLUTE TIME', exports.SimVarValueType.Seconds) - 62135596800) * 1000;
            this.bus.getSubscriber().on('gps-position').handle(pos => {
                this.ppos.set(pos.lat, pos.long);
                Vec2Math.set(pos.lat, pos.long, this.pposVec);
                this.altitude = pos.alt;
            });
            this.bus.getSubscriber().on('simTime').handle(time => this.simTime = time);
        }
        /**
         * Adds the defined SBAS satellites to the tracked satellites.
         */
        addSbasSatellites() {
            const tempVec = new Float64Array(3);
            const tempGeoPoint = new GeoPoint(0, 0);
            const orbitHeight = UnitType.KILOMETER.convertTo(35785, UnitType.GA_RADIAN);
            for (let i = 0; i < this.sbasData.length; i++) {
                const sbasDef = this.sbasData[i];
                this.sbasServiceAreas.set(sbasDef.group, sbasDef.coverage);
                for (const satDef of sbasDef.constellation) {
                    const sat = new GPSSatellite(satDef.prn, sbasDef.group, undefined, this.satelliteTimingOptions);
                    tempGeoPoint.set(0, satDef.lon);
                    const positionCartesian = Vec3Math.multScalar(tempGeoPoint.toCartesian(tempVec), orbitHeight, tempVec);
                    sat.positionCartesian.set(positionCartesian);
                    this.satellites.push(sat);
                }
            }
        }
        /** @inheritdoc */
        init() {
            // Publish initial state.
            this.publisher.pub(this.nominalChannelCountTopic, this.nominalChannelCount, false, true);
            this.publisher.pub(this.stateChangedTopic, this._state, false, true);
            this.publisher.pub(this.sbasStateChangedTopic, this._sbasState, false, true);
            this.publisher.pub(this.pdopTopic, this._pdop, false, true);
            this.publisher.pub(this.hdopTopic, this._hdop, false, true);
            this.publisher.pub(this.vdopTopic, this._vdop, false, true);
            this.loadEphemerisData().then(() => this.loadSbasData()).then(() => {
                this.publishedSatStates.length = this.satellites.length;
                for (let i = 0; i < this.satellites.length; i++) {
                    const sat = this.satellites[i];
                    this.publishedSatStates[i] = { prn: sat.prn, state: exports.GPSSatelliteState.None, areDiffCorrectionsDownloaded: false };
                }
                this._channelCount = Math.min(this._channelCount, this.satellites.length);
                this.channels.length = this._channelCount;
                this.channels.fill(null);
                this.isInit = true;
                // Setup sync logic.
                if (this.syncRole === 'replica') {
                    const sub = this.bus.getSubscriber();
                    const copyEnabledSbasGroups = (groups) => {
                        this.enabledSBASGroupsSet.clear();
                        for (const group of groups) {
                            this.enabledSBASGroupsSet.add(group);
                        }
                    };
                    sub.on(this.channelStateSyncTopic).handle(data => { this.pendingChannelStateUpdates.set(data.index, data); });
                    sub.on(this.satCalcSyncTopic).handle(() => { this.needSatCalc = true; });
                    sub.on(this.satStateSyncTopic).handle(data => { this.pendingSatStateUpdates.set(data.prn, data); });
                    sub.on(this.enabledSbasSyncTopic).handle(copyEnabledSbasGroups);
                    sub.on(this.resetSyncTopic).handle(() => { this.reset(); });
                    sub.on(this.stateResponseSyncTopic).handle(response => {
                        this.needSatCalc = true;
                        for (const channelState of response.channels) {
                            this.pendingChannelStateUpdates.set(channelState.index, channelState);
                        }
                        for (const satState of response.satStates) {
                            this.pendingSatStateUpdates.set(satState.prn, satState);
                        }
                        copyEnabledSbasGroups(response.enabledSbasGroups);
                    });
                    // Request initial state.
                    this.syncPublisher.pub(this.stateRequestSyncTopic, undefined, true, false);
                }
                else if (this.syncRole === 'primary') {
                    const sub = this.bus.getSubscriber();
                    sub.on(this.stateRequestSyncTopic).handle(() => {
                        this.syncPublisher.pub(this.stateResponseSyncTopic, {
                            channels: this.channels.map((sat, index) => { return { index, prn: sat === null ? null : sat.prn }; }),
                            satStates: this.satellites.map(sat => { return { prn: sat.prn, state: sat.state.get(), areDiffCorrectionsDownloaded: sat.areDiffCorrectionsDownloaded }; }),
                            enabledSbasGroups: Array.from(this.enabledSBASGroups.get())
                        }, true, false);
                    });
                    this.enabledSBASGroups.sub(groups => {
                        this.syncPublisher.pub(this.enabledSbasSyncTopic, Array.from(groups), true, false);
                    });
                }
                if (this.needAcquireAndUse) {
                    this.needAcquireAndUse = false;
                    this.acquireAndUseSatellites();
                }
                else {
                    this.reset();
                }
            });
        }
        /**
         * Loads the GPS ephemeris data file.
         */
        loadEphemerisData() {
            return new Promise((resolve, reject) => {
                const request = new XMLHttpRequest();
                request.onreadystatechange = () => {
                    if (request.readyState === XMLHttpRequest.DONE) {
                        if (request.status === 200) {
                            this.ephemerisData = JSON.parse(request.responseText);
                            for (const prn in this.ephemerisData) {
                                this.satellites.push(new GPSSatellite(parseInt(prn), undefined, this.ephemerisData[prn], this.satelliteTimingOptions));
                            }
                            resolve();
                        }
                        else {
                            reject(`Could not initialize sat computer system with ephemeris data: ${request.responseText}`);
                        }
                    }
                };
                request.open('GET', this.ephemerisFile);
                request.send();
            });
        }
        /**
         * Loads the GPS SBAS data file.
         */
        loadSbasData() {
            return new Promise((resolve, reject) => {
                const request = new XMLHttpRequest();
                request.onreadystatechange = () => {
                    if (request.readyState === XMLHttpRequest.DONE) {
                        if (request.status === 200) {
                            this.sbasData = JSON.parse(request.responseText);
                            this.addSbasSatellites();
                            resolve();
                        }
                        else {
                            reject(`Could not initialize sat computer system with sbas data: ${request.responseText}`);
                        }
                    }
                };
                request.open('GET', this.sbasFile);
                request.send();
            });
        }
        /**
         * Gets the index of a satellite with a given PRN identifier.
         * @param prn The PRN identifier for which to get the satellite index.
         * @returns The index of the satellite with the specified PRN identifier, or `-1` if the PRN does not belong to any
         * satellite.
         */
        getSatelliteIndexFromPrn(prn) {
            for (let i = 0; i < this.satellites.length; i++) {
                if (this.satellites[i].prn === prn) {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Calculates the horizon zenith angle.
         * @returns The calculated horizon zenith angle based on the current altitude.
         */
        calcHorizonAngle() {
            return Math.acos(6378100 / (6378100 + this.altitude));
        }
        /**
         * Syncs this computer's last known position with a given value.
         * @param pos The position with which to sync the last known position. Defaults to the airplane's current position.
         */
        syncLastKnownPosition(pos = this.ppos) {
            this.lastKnownPosition.set(pos);
        }
        /**
         * Erases this computer's last known position.
         */
        eraseLastKnownPosition() {
            this.lastKnownPosition.set(NaN, NaN);
        }
        /**
         * Checks whether this computer's downloaded almanac data is valid at a given simulation time.
         * @param simTime The simulation time at which to check for almanac validity, as a Javascript timestamp. Defaults to
         * the current simulation time.
         * @returns Whether this computer's downloaded almanac data is valid at the specified simulation time.
         */
        isAlmanacValid(simTime = this.simTime) {
            return this.lastAlamanacTime !== undefined && Math.abs(simTime - this.lastAlamanacTime) < this.satelliteTimingOptions.almanacExpireTime;
        }
        /**
         * Forces this computer to immediately download a complete alamanac.
         * @param simTime The simulation time at which the almanac is considered to have been downloaded, as a Javascript
         * timestamp. Defaults to the current simulation time.
         */
        downloadAlamanac(simTime = this.simTime) {
            this.almanacProgress = 0;
            this.lastAlamanacTime = simTime;
        }
        /**
         * Erases this computer's downloaded almanac and any partial download progress.
         */
        eraseAlamanac() {
            this.almanacProgress = 0;
            this.lastAlamanacTime = undefined;
        }
        /**
         * Erases this computer's cached ephemeris data for all satellites.
         */
        eraseCachedEphemeris() {
            for (let i = 0; i < this.satellites.length; i++) {
                this.satellites[i].eraseCachedEphemeris();
            }
        }
        /**
         * Instantly chooses the optimal satellites to track for all receiver channels, then acquires and downloads all data
         * (ephemeris, almanac, and differential corrections) from tracked satellites with sufficient signal strength. If
         * this system is not initialized, the operation will be delayed until just after initialization, unless `reset()` is
         * called in the interim.
         *
         * Has no effect if this system is a replica.
         */
        acquireAndUseSatellites() {
            if (this.syncRole === 'replica') {
                return;
            }
            if (this.isInit) {
                this.updateSatellites(this.simTime, 0, true, true);
            }
            else {
                this.needAcquireAndUse = true;
            }
        }
        /**
         * Resets the GPSSatComputer system. This will set the state of the system to {@link GPSSystemState.Searching},
         * unassign all receiver channels, and set the state of every satellite to {@link GPSSatelliteState.None}.
         *
         * If this system is not initialized, this method has no effect other than to cancel any pending operations triggered
         * by previous calls to `acquireAndUseSatellites()`.
         */
        reset() {
            this.needAcquireAndUse = false;
            if (!this.isInit) {
                return;
            }
            for (let i = 0; i < this.channels.length; i++) {
                this.channels[i] = null;
            }
            for (const sat of this.satellites) {
                const oldState = sat.state.get();
                sat.setTracked(false);
                sat.state.set(exports.GPSSatelliteState.None);
                if (oldState !== exports.GPSSatelliteState.None) {
                    this.publisher.pub(this.satStateChangedTopic, sat, false, false);
                }
            }
            const currentState = this._state;
            this._state = exports.GPSSystemState.Searching;
            if (currentState !== exports.GPSSystemState.Searching) {
                this.publisher.pub(this.stateChangedTopic, exports.GPSSystemState.Searching, false, true);
            }
            this.setDop(-1, -1, -1);
            if (this.syncRole === 'primary') {
                this.syncPublisher.pub(this.resetSyncTopic, undefined, true, false);
            }
        }
        /** @inheritdoc */
        onUpdate() {
            if (!this.isInit) {
                return;
            }
            const deltaTime = this.simTime - this.previousSimTime;
            if (this.syncRole !== 'replica') {
                if (deltaTime < 0 || deltaTime > (this.updateInterval * 2)) {
                    this.previousSimTime = this.simTime;
                    if (this.lastUpdateTime !== undefined) {
                        this.lastUpdateTime = this.simTime;
                    }
                    return;
                }
            }
            const shouldUpdatePositions = this.syncRole === 'replica'
                ? this.needSatCalc
                : this.lastUpdateTime === undefined || this.simTime >= this.lastUpdateTime + this.updateInterval;
            this.needSatCalc = false;
            this.updateSatellites(this.simTime, deltaTime, shouldUpdatePositions, false);
        }
        /**
         * Updates the states and optionally the orbital positions of all satellites.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @param deltaTime The time elapsed, in milliseconds, since the last satellite update.
         * @param shouldUpdatePositions Whether to update the orbital positions of the satellites.
         * @param forceAcquireAndUse Whether to immediately choose the optimal satellites to track for all receiver channels,
         * then acquire and download all data (ephemeris, almanac, and differential corrections) from tracked satellites with
         * sufficient signal strength.
         */
        updateSatellites(simTime, deltaTime, shouldUpdatePositions, forceAcquireAndUse) {
            var _a, _b;
            let numAcquiring = 0;
            let canApplyDiffCorrections = false;
            let shouldUpdateDop = shouldUpdatePositions;
            if (shouldUpdatePositions && this.syncRole === 'primary') {
                (_a = this.syncPublisher) === null || _a === void 0 ? void 0 : _a.pub(this.satCalcSyncTopic, undefined, true, false);
            }
            if (forceAcquireAndUse) {
                this.lastKnownPosition.set(this.ppos);
            }
            this.distanceFromLastKnownPos = isNaN(this.lastKnownPosition.lat) || isNaN(this.lastKnownPosition.lon)
                ? Infinity
                : this.ppos.distance(this.lastKnownPosition);
            this._isAlmanacValid = this.isAlmanacValid();
            this.updateAvailableSbasGroups();
            const enabledSBASGroups = this.enabledSBASGroups.get();
            const invMaxZenithAngle = 1.0 / (GPSSatellite.calcHorizonAngle(this.altitude) + (Math.PI / 2));
            for (let i = 0; i < this.satellites.length; i++) {
                const sat = this.satellites[i];
                if (shouldUpdatePositions) {
                    sat.computeSatellitePositions(this.simTime);
                    sat.applyProjection(this.ppos, this.altitude);
                }
                sat.calculateSignalStrength(invMaxZenithAngle);
            }
            if (this.syncRole === 'replica') {
                for (const update of this.pendingChannelStateUpdates.values()) {
                    const sat = update.prn === null ? null : ((_b = this.satellites[this.getSatelliteIndexFromPrn(update.prn)]) !== null && _b !== void 0 ? _b : null);
                    this.assignSatelliteToChannel(update.index, sat);
                }
            }
            else if (shouldUpdatePositions) {
                this.updateChannelAssignments(forceAcquireAndUse);
            }
            this.pendingChannelStateUpdates.clear();
            for (let i = 0; i < this.satellites.length; i++) {
                const sat = this.satellites[i];
                let updatedState;
                if (this.syncRole === 'replica') {
                    const stateUpdate = this.pendingSatStateUpdates.get(sat.prn);
                    updatedState = sat.forceUpdateState(simTime, stateUpdate === null || stateUpdate === void 0 ? void 0 : stateUpdate.state, stateUpdate === null || stateUpdate === void 0 ? void 0 : stateUpdate.areDiffCorrectionsDownloaded);
                }
                else {
                    updatedState = sat.updateState(simTime, deltaTime, this.distanceFromLastKnownPos, forceAcquireAndUse);
                }
                if (updatedState) {
                    shouldUpdateDop = true;
                }
                const satState = sat.state.get();
                if (satState === exports.GPSSatelliteState.DataCollected
                    || satState === exports.GPSSatelliteState.InUse
                    || satState === exports.GPSSatelliteState.InUseDiffApplied) {
                    numAcquiring++;
                    if (sat.areDiffCorrectionsDownloaded && this.currentAvailableSbasGroups.has(sat.sbasGroup)) {
                        canApplyDiffCorrections = true;
                    }
                }
                else if (satState === exports.GPSSatelliteState.Acquired) {
                    numAcquiring++;
                }
            }
            this.pendingSatStateUpdates.clear();
            const newSBASState = canApplyDiffCorrections
                ? exports.GPSSystemSBASState.Active
                : enabledSBASGroups.size === 0 ? exports.GPSSystemSBASState.Disabled : exports.GPSSystemSBASState.Inactive;
            let pdop = this._pdop, hdop = this._hdop, vdop = this._vdop;
            if (shouldUpdateDop) {
                if (this.syncRole !== 'replica') {
                    [pdop, hdop, vdop] = this.selectSatellites(this.dops);
                }
                else if (shouldUpdateDop) {
                    [pdop, hdop, vdop] = this.calculateDop(this.dops);
                }
            }
            let newSystemState = exports.GPSSystemState.Searching;
            if (pdop >= 0) {
                newSystemState = canApplyDiffCorrections ? exports.GPSSystemState.DiffSolutionAcquired : exports.GPSSystemState.SolutionAcquired;
                this.lastKnownPosition.set(this.ppos);
            }
            else if (numAcquiring > 0) {
                newSystemState = exports.GPSSystemState.Acquiring;
            }
            else if (this.distanceFromLastKnownPos < 0.0290367 /* 100 nautical miles */) {
                // Set system state to 'Acquiring' if we are attempting to acquire at least one satellite for which we have
                // predicted geometry data (either from the almanac or cached ephemeris data).
                for (let i = 0; i < this.channels.length; i++) {
                    const sat = this.channels[i];
                    if (sat && sat.state.get() === exports.GPSSatelliteState.None && (this._isAlmanacValid || sat.isCachedEphemerisValid(this.simTime))) {
                        newSystemState = exports.GPSSystemState.Acquiring;
                        break;
                    }
                }
            }
            if (this.syncRole !== 'replica') {
                for (let i = 0; i < this.channels.length; i++) {
                    const sat = this.channels[i];
                    if (sat) {
                        sat.updateDiffCorrectionsApplied(canApplyDiffCorrections);
                    }
                }
                this.updateAlmanacState(simTime, deltaTime, forceAcquireAndUse);
            }
            this.diffAndPublishSatelliteStates();
            if (this._state !== newSystemState) {
                this._state = newSystemState;
                this.publisher.pub(this.stateChangedTopic, newSystemState, false, true);
            }
            if (this._sbasState !== newSBASState) {
                this._sbasState = newSBASState;
                this.publisher.pub(this.sbasStateChangedTopic, newSBASState, false, true);
            }
            if (shouldUpdatePositions) {
                this.lastUpdateTime = this.simTime;
                this.publisher.pub(this.satPosCalcTopic, undefined, false, false);
            }
            this.setDop(pdop, hdop, vdop);
            this.previousSimTime = this.simTime;
        }
        /**
         * Updates which SBAS groups are enabled and whose coverage area contain the airplane's current position.
         */
        updateAvailableSbasGroups() {
            const enabledSBASGroups = this.enabledSBASGroups.get();
            for (let i = 0; i < this.sbasData.length; i++) {
                const sbasData = this.sbasData[i];
                if (enabledSBASGroups.has(sbasData.group) && Vec2Math.pointWithinPolygon(sbasData.coverage, this.pposVec)) {
                    this.currentAvailableSbasGroups.add(sbasData.group);
                }
                else {
                    this.currentAvailableSbasGroups.delete(sbasData.group);
                }
            }
        }
        /**
         * Updates the satellites assigned to be tracked by this computer's receiver channels.
         * @param forceAcquireAndUse Whether to immediately choose the optimal satellites to track and acquire all data from
         * tracked satellites if signal strength is sufficient.
         */
        updateChannelAssignments(forceAcquireAndUse) {
            // If we have at least one channel for every satellite, then we will simply assign each satellite to its own
            // channel.
            if (this._channelCount >= this.satellites.length) {
                const end = Math.min(this._channelCount, this.satellites.length);
                for (let i = 0; i < end; i++) {
                    if (this.channels[i] === null) {
                        this.assignSatelliteToChannel(i, this.satellites[i]);
                    }
                }
                return;
            }
            const losSatellites = this.satellites.filter(forceAcquireAndUse ? this.losSatelliteFilterOmniscient : this.losSatelliteFilter);
            let losSatellitesNotTrackedIndexes;
            let openChannelIndexes;
            let isTrackingSbasSatelliteInLos = false;
            if (forceAcquireAndUse) {
                losSatellitesNotTrackedIndexes = ArrayUtils.range(losSatellites.length);
                openChannelIndexes = ArrayUtils.range(this._channelCount, this._channelCount - 1, -1);
            }
            else {
                losSatellitesNotTrackedIndexes = [];
                for (let i = 0; i < losSatellites.length; i++) {
                    const sat = losSatellites[i];
                    if (this.channels.includes(sat)) {
                        if (sat.sbasGroup !== undefined && this.currentAvailableSbasGroups.has(sat.sbasGroup)) {
                            isTrackingSbasSatelliteInLos = true;
                        }
                    }
                    else {
                        losSatellitesNotTrackedIndexes.push(i);
                    }
                }
                openChannelIndexes = [];
                for (let i = this.channels.length - 1; i >= 0; i--) {
                    const sat = this.channels[i];
                    if (!sat || sat.state.get() === exports.GPSSatelliteState.Unreachable) {
                        openChannelIndexes.push(i);
                    }
                }
            }
            if (openChannelIndexes.length === 0 && this.channels.every(this.ephemerisCollectedSatelliteFilter)) {
                // There are no open channels and we have collected ephemeris data from every tracked satellite.
                const trackedLosMatrix = GPSSatComputer.getLosMatrix(this.channels);
                const trackedCovarMatrix = GPSSatComputer.calculateCovarMatrix(trackedLosMatrix, this.covarMatrix);
                if (!isFinite(trackedCovarMatrix[0][0]) || !isFinite(trackedCovarMatrix[1][1]) || !isFinite(trackedCovarMatrix[2][2])) {
                    // The currently tracked satellites are not sufficient to produce a 3D position solution. In this case we
                    // will replace a random tracked satellite with an untracked.
                    openChannelIndexes.push(Math.trunc(Math.random() * this._channelCount));
                }
                else {
                    // The currently tracked satellites are sufficient to produce a 3D position solution. In this case we will
                    // only try to replace a tracked satellite if we are tracking at least one redundant satellite, we are not
                    // tracking an SBAS satellite within LOS, and there is a SBAS satellite within LOS available for us to track.
                    // If the above is true, then we will replace the tracked satellite with the smallest contribution to reducing
                    // PDOP with the SBAS satellite with highest signal strength.
                    if (this._channelCount > 4 && !isTrackingSbasSatelliteInLos) {
                        let highestSbasSignal = 0;
                        let highestSbasSignalIndex = -1;
                        for (let i = 0; i < losSatellitesNotTrackedIndexes.length; i++) {
                            const index = losSatellitesNotTrackedIndexes[i];
                            const sat = losSatellites[index];
                            const signalStrength = sat.signalStrength.get();
                            if (sat.sbasGroup !== undefined && this.currentAvailableSbasGroups.has(sat.sbasGroup) && signalStrength > highestSbasSignal) {
                                highestSbasSignal = signalStrength;
                                highestSbasSignalIndex = index;
                            }
                        }
                        if (highestSbasSignalIndex >= 0) {
                            const sTranspose = this.channels.map(GPSSatComputer.createVec4);
                            GPSSatComputer.calculateDowndateSTranspose(trackedLosMatrix, trackedCovarMatrix, sTranspose);
                            const pDiag = GPSSatComputer.calculateDowndatePDiag(trackedLosMatrix, sTranspose, new Float64Array(trackedLosMatrix.length));
                            GPSSatComputer.calculateSatelliteCosts(sTranspose, pDiag, this.satelliteCosts);
                            let satToReplaceCost = Infinity;
                            let satToReplaceChannelIndex = -1;
                            for (let i = 0; i < this.channels.length; i++) {
                                const cost = this.satelliteCosts[i];
                                if (cost < satToReplaceCost) {
                                    satToReplaceCost = cost;
                                    satToReplaceChannelIndex = i;
                                }
                            }
                            if (satToReplaceChannelIndex >= 0) {
                                this.assignSatelliteToChannel(satToReplaceChannelIndex, losSatellites[highestSbasSignalIndex]);
                            }
                        }
                    }
                    return;
                }
            }
            if (openChannelIndexes.length > 0) {
                if (openChannelIndexes.length < losSatellitesNotTrackedIndexes.length) {
                    // We don't have enough open channels to begin tracking all satellites currently within line-of-sight.
                    // Therefore, we will choose those with the largest contribution to reducing PDOP.
                    const losMatrix = GPSSatComputer.getLosMatrix(losSatellites);
                    const covarMatrix = GPSSatComputer.calculateCovarMatrix(losMatrix, this.covarMatrix);
                    const sTranspose = losSatellites.map(GPSSatComputer.createVec4);
                    GPSSatComputer.calculateDowndateSTranspose(losMatrix, covarMatrix, sTranspose);
                    const pDiag = GPSSatComputer.calculateDowndatePDiag(losMatrix, sTranspose, new Float64Array(losMatrix.length));
                    GPSSatComputer.calculateSatelliteCosts(sTranspose, pDiag, this.satelliteCosts);
                    // If we are not already tracking an SBAS satellite within LOS, we will prioritize adding the SBAS satellite
                    // with the highest cost over non-SBAS satellites.
                    if (!isTrackingSbasSatelliteInLos) {
                        let highestSbasCost = -Infinity;
                        let highestSbasCostIndex = -1;
                        for (let i = 0; i < this.satelliteCosts.length; i++) {
                            const sbasGroup = losSatellites[i].sbasGroup;
                            if (sbasGroup !== undefined && this.currentAvailableSbasGroups.has(sbasGroup)) {
                                const cost = this.satelliteCosts[i];
                                if (cost > highestSbasCost) {
                                    highestSbasCost = cost;
                                    highestSbasCostIndex = i;
                                }
                            }
                        }
                        if (highestSbasCostIndex >= 0) {
                            this.satelliteCosts[highestSbasCostIndex] = Infinity;
                        }
                    }
                    const satelliteIndexes = ArrayUtils.range(losSatellites.length);
                    satelliteIndexes.sort(this.satelliteCostCompare);
                    for (let i = satelliteIndexes.length - 1; i >= 0; i--) {
                        const satIndex = satelliteIndexes[i];
                        if (losSatellitesNotTrackedIndexes.includes(satIndex)) {
                            const sat = losSatellites[satIndex];
                            const channelIndex = openChannelIndexes.pop();
                            this.assignSatelliteToChannel(channelIndex, sat);
                            if (openChannelIndexes.length === 0) {
                                break;
                            }
                        }
                    }
                }
                else {
                    // We have enough open channels to begin tracking all satellites currently within LOS.
                    for (let i = 0; i < losSatellitesNotTrackedIndexes.length; i++) {
                        const satIndex = losSatellitesNotTrackedIndexes[i];
                        const channelIndex = openChannelIndexes.pop();
                        this.assignSatelliteToChannel(channelIndex, losSatellites[satIndex]);
                    }
                }
                // If we still have open channels available, fill them with random satellites that have not been marked as
                // unreachable.
                if (openChannelIndexes.length > 0) {
                    const untrackedSatellites = this.satellites.filter(this.untrackedSatelliteFilter);
                    let untrackedIndex = 0;
                    while (openChannelIndexes.length > 0 && untrackedIndex < untrackedSatellites.length) {
                        const channelIndex = openChannelIndexes.pop();
                        this.assignSatelliteToChannel(channelIndex, untrackedSatellites[untrackedIndex++]);
                    }
                }
            }
        }
        /**
         * Assigns a satellite to a receiver channel.
         * @param channelIndex The index of the receiver channel.
         * @param sat The satellite to assign, or `null` if the channel is to be assigned no satellite.
         */
        assignSatelliteToChannel(channelIndex, sat) {
            const oldSat = this.channels[channelIndex];
            if (oldSat === sat) {
                return;
            }
            if (oldSat) {
                oldSat.setTracked(false);
            }
            this.channels[channelIndex] = sat;
            if (sat) {
                sat.setTracked(true);
            }
            if (this.syncRole === 'primary') {
                this.syncPublisher.pub(this.channelStateSyncTopic, { index: channelIndex, prn: sat === null ? null : sat.prn }, true, false);
            }
        }
        /**
         * Calculates dilution of precision values (PDOP, HDOP, VDOP) for the satellite constellation consisting of all
         * satellites that are currently in-use.
         * @param out The vector to which to write the results.
         * @returns Dilution of precision values for the current in-use satellite constellation, as `[PDOP, HDOP, VDOP]`. If
         * the constellation is insufficient to provide a 3D position solution, then `[-1, -1, -1]` will be returned.
         */
        calculateDop(out) {
            Vec3Math.set(-1, -1, -1, out);
            const satsInUse = this.satellites.filter(GPSSatComputer.inUseSatelliteFilter);
            if (satsInUse.length < 4) {
                return out;
            }
            const losMatrix = GPSSatComputer.getLosMatrix(satsInUse);
            const covarMatrix = GPSSatComputer.calculateCovarMatrix(losMatrix, this.covarMatrix);
            // Grab the variance terms var(x), var(y), var(z) along the diagonal of the covariance matrix
            const varX = covarMatrix[0][0];
            const varY = covarMatrix[1][1];
            const varZ = covarMatrix[2][2];
            if (!isFinite(varX) || !isFinite(varY) || !isFinite(varZ)) {
                return out;
            }
            const horizSumVar = varX + varY;
            const pdop = Math.sqrt(horizSumVar + varZ);
            const hdop = Math.sqrt(horizSumVar);
            const vdop = Math.sqrt(varZ);
            return Vec3Math.set(pdop, hdop, vdop, out);
        }
        /**
         * Selects satellites to use for calculating position solutions and returns the dilution of precision values for
         * the selected constellation.
         * @param out The vector to which to write the dilution of precision values.
         * @returns Dilution of precision values for the selected satellite constellation, as `[PDOP, HDOP, VDOP]`. If the
         * constellation is insufficient to provide a 3D position solution, then `[-1, -1, -1]` will be returned.
         */
        selectSatellites(out) {
            Vec3Math.set(-1, -1, -1, out);
            const satellitesToUse = this.satellites.filter(GPSSatComputer.readySatelliteFilter);
            if (satellitesToUse.length < 4) {
                this.updateSatelliteInUseStates(satellitesToUse, []);
                return out;
            }
            const losMatrix = GPSSatComputer.getLosMatrix(satellitesToUse);
            const covarMatrix = GPSSatComputer.calculateCovarMatrix(losMatrix, this.covarMatrix);
            const maxCount = MathUtils.clamp(this.satInUseMaxCount.get(), 4, this._channelCount);
            if (!VecNMath.isFinite(covarMatrix[0])
                || !VecNMath.isFinite(covarMatrix[1])
                || !VecNMath.isFinite(covarMatrix[2])
                || !VecNMath.isFinite(covarMatrix[3])) {
                const satellitesToDiscard = satellitesToUse.splice(maxCount);
                this.updateSatelliteInUseStates(satellitesToUse, satellitesToDiscard);
                return out;
            }
            const satellitesToDiscard = [];
            const pdopTarget = this.satInUsePdopTarget.get();
            const optimumCount = Math.max(this.satInUseOptimumCount.get(), 4);
            const pdopTargetSq = pdopTarget < 0 ? -1 : pdopTarget * pdopTarget;
            let pdopSq = covarMatrix[0][0] + covarMatrix[1][1] + covarMatrix[2][2];
            if (satellitesToUse.length > maxCount || (satellitesToUse.length > optimumCount && pdopSq < pdopTargetSq)) {
                // There are more in-sight satellites than we can select. Therefore we will attempt to discard excess satellites
                // in manner that minimizes the increase to PDOP relative to selecting all in-sight satellites.
                // We will use the "downdate" selection algorithm presented in Walter, T, Blanch, J and Kropp, V, 2016.
                // Define Sᵀ = LC and P = I - LCLᵀ, where L is the line-of-sight matrix and C is the covariance matrix.
                // Then Ci = C + (Si)(Si)ᵀ / P(i, i), where Ci is the covariance matrix after removing the ith satellite and
                // Si is the ith column of S.
                // If PDOP = sqrt(C(1, 1) + C(2, 2) + C(3, 3)), then from the above it can be seen that removing the ith
                // satellite increases PDOP² by (S(1, i)² + S(2, i)² + S(3, i)²) / P(i, i). Defining this to be the cost of
                // removing satellite i, we are then guaranteed that removing the satellite with the lowest cost will result
                // in the smallest increase to PDOP.
                const sTranspose = satellitesToUse.map(GPSSatComputer.createVec4);
                GPSSatComputer.calculateDowndateSTranspose(losMatrix, covarMatrix, sTranspose);
                const pDiag = GPSSatComputer.calculateDowndatePDiag(losMatrix, sTranspose, new Float64Array(losMatrix.length));
                GPSSatComputer.calculateSatelliteCosts(sTranspose, pDiag, this.satelliteCosts);
                const satelliteIndexes = ArrayUtils.range(satellitesToUse.length);
                satelliteIndexes.sort(this.satelliteCostCompare);
                pdopSq = covarMatrix[0][0] + covarMatrix[1][1] + covarMatrix[2][2];
                let indexToRemove = satelliteIndexes[0];
                while (satellitesToUse.length > maxCount
                    || (satellitesToUse.length > optimumCount
                        && pdopSq + this.satelliteCosts[indexToRemove] <= pdopTargetSq)) {
                    satellitesToDiscard.push(satellitesToUse[indexToRemove]);
                    satellitesToUse.splice(indexToRemove, 1);
                    losMatrix.splice(indexToRemove, 1);
                    // Reset satellite index array.
                    satelliteIndexes.length--;
                    for (let i = 0; i < satelliteIndexes.length; i++) {
                        satelliteIndexes[i] = i;
                    }
                    // Update covariance matrix after removing a satellite.
                    for (let i = 0; i < 4; i++) {
                        for (let j = 0; j < 4; j++) {
                            covarMatrix[i][j] += sTranspose[indexToRemove][i] * sTranspose[indexToRemove][j] / pDiag[indexToRemove];
                        }
                    }
                    // Recompute satellite costs.
                    sTranspose.length--;
                    GPSSatComputer.calculateDowndateSTranspose(losMatrix, covarMatrix, sTranspose);
                    GPSSatComputer.calculateDowndatePDiag(losMatrix, sTranspose, pDiag);
                    GPSSatComputer.calculateSatelliteCosts(sTranspose, pDiag, this.satelliteCosts);
                    satelliteIndexes.sort(this.satelliteCostCompare);
                    pdopSq = covarMatrix[0][0] + covarMatrix[1][1] + covarMatrix[2][2];
                    indexToRemove = satelliteIndexes[0];
                }
            }
            this.updateSatelliteInUseStates(satellitesToUse, satellitesToDiscard);
            // Grab the variance terms var(x), var(y), var(z) along the diagonal of the covariance matrix
            const varX = covarMatrix[0][0];
            const varY = covarMatrix[1][1];
            const varZ = covarMatrix[2][2];
            if (!isFinite(varX) || !isFinite(varY) || !isFinite(varZ)) {
                return out;
            }
            const horizSumVar = varX + varY;
            const pdop = Math.sqrt(horizSumVar + varZ);
            const hdop = Math.sqrt(horizSumVar);
            const vdop = Math.sqrt(varZ);
            return Vec3Math.set(pdop, hdop, vdop, out);
        }
        /**
         * Updates the in-use state of satellites.
         * @param satellitesToUse The satellites to use for position solution calculations.
         * @param satellitesToNotUse The satellites to not use for position solution calculations.
         */
        updateSatelliteInUseStates(satellitesToUse, satellitesToNotUse) {
            for (let i = 0; i < satellitesToUse.length; i++) {
                satellitesToUse[i].updateInUse(true);
            }
            for (let i = 0; i < satellitesToNotUse.length; i++) {
                satellitesToNotUse[i].updateInUse(false);
            }
        }
        /**
         * Updates the almanac download state.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @param deltaTime The time elapsed, in milliseconds, since the last update.
         * @param forceDownload Whether to force the entire almanac to be instantly downloaded.
         */
        updateAlmanacState(simTime, deltaTime, forceDownload) {
            if (forceDownload) {
                this.lastAlamanacTime = simTime;
                this.almanacProgress = 0;
            }
            else {
                const isCollectingData = this.channels.some(this.collectingDataSatelliteFilter);
                if (isCollectingData) {
                    this.almanacProgress += deltaTime / this.satelliteTimingOptions.almanacDownloadTime;
                    if (this.almanacProgress >= 1) {
                        this.lastAlamanacTime = simTime;
                        this.almanacProgress -= 1;
                    }
                }
                else {
                    this.almanacProgress = 0;
                }
            }
        }
        /**
         * For each satellite, checks if its state is different from the most recently published state, and if so publishes
         * the new state. If this computer's sync role is `primary`, then a satellite state sync event will be published
         * alongside any regular state events.
         */
        diffAndPublishSatelliteStates() {
            for (let i = 0; i < this.satellites.length; i++) {
                const publishedState = this.publishedSatStates[i];
                const sat = this.satellites[i];
                const state = sat.state.get();
                const areDiffCorrectionsDownloaded = sat.areDiffCorrectionsDownloaded;
                const needPublishState = publishedState.state !== state;
                const needSyncState = needPublishState || publishedState.areDiffCorrectionsDownloaded !== areDiffCorrectionsDownloaded;
                if (needSyncState) {
                    publishedState.state = state;
                    publishedState.areDiffCorrectionsDownloaded = areDiffCorrectionsDownloaded;
                    if (needPublishState) {
                        this.publisher.pub(this.satStateChangedTopic, sat, false, false);
                    }
                    if (this.syncRole === 'primary') {
                        this.syncPublisher.pub(this.satStateSyncTopic, { prn: sat.prn, state, areDiffCorrectionsDownloaded }, true, false);
                    }
                }
            }
        }
        /**
         * Sets this system's dilution of precision values, and if they are different from the current values, publishes the
         * new values to the event bus.
         * @param pdop The position DOP value to set.
         * @param hdop The horizontal DOP value to set.
         * @param vdop The vertical DOP value to set.
         */
        setDop(pdop, hdop, vdop) {
            if (this._pdop !== pdop) {
                this._pdop = pdop;
                this.publisher.pub(this.pdopTopic, pdop, false, true);
            }
            if (this._hdop !== hdop) {
                this._hdop = hdop;
                this.publisher.pub(this.hdopTopic, hdop, false, true);
            }
            if (this._vdop !== vdop) {
                this._vdop = vdop;
                this.publisher.pub(this.vdopTopic, vdop, false, true);
            }
        }
        /**
         * Creates a line-of-sight position matrix for a satellite constellation. Each row in the matrix is a 4-vector of
         * a satellite's position relative to the airplane, as `[x, y, z, 1]`. The index of the matrix row containing a
         * satellite's position vector matches the index of the satellite in the provided array.
         * @param satellites The satellites in the constellation.
         * @returns The line-of-sight position matrix for the specified satellite constellation.
         */
        static getLosMatrix(satellites) {
            const los = [];
            // Get unit line-of-sight vectors for each satellite
            for (let i = 0; i < satellites.length; i++) {
                const [zenith, hour] = satellites[i].position.get();
                los[i] = Vec3Math.setFromSpherical(1, zenith, hour, new Float64Array(4));
                los[i][3] = 1;
            }
            return los;
        }
        /**
         * Calculates a position-covariance matrix for a satellite constellation.
         * @param los The line-of-sight position matrix for the satellite constellation.
         * @param out The matrix to which to write the result.
         * @returns The position-covariance matrix for the specified satellite constellation.
         */
        static calculateCovarMatrix(los, out) {
            if (los.length < 4) {
                for (let i = 0; i < 4; i++) {
                    out[i].fill(NaN, 0, 4);
                }
                return out;
            }
            // The covariance matrix is defined as C = (LᵀL)⁻¹, where L is the satellite line-of-sight matrix.
            // P = LᵀL is guaranteed to be symmetric, so we need only compute the upper triangular part of the product.
            const P11 = los.reduce(GPSSatComputer.covarMultiplyFuncs[0][0], 0);
            const P12 = los.reduce(GPSSatComputer.covarMultiplyFuncs[0][1], 0);
            const P13 = los.reduce(GPSSatComputer.covarMultiplyFuncs[0][2], 0);
            const P14 = los.reduce(GPSSatComputer.covarMultiplyFuncs[0][3], 0);
            const P22 = los.reduce(GPSSatComputer.covarMultiplyFuncs[1][0], 0);
            const P23 = los.reduce(GPSSatComputer.covarMultiplyFuncs[1][1], 0);
            const P24 = los.reduce(GPSSatComputer.covarMultiplyFuncs[1][2], 0);
            const P33 = los.reduce(GPSSatComputer.covarMultiplyFuncs[2][0], 0);
            const P34 = los.reduce(GPSSatComputer.covarMultiplyFuncs[2][1], 0);
            const P44 = los.length;
            // Perform block-wise inversion of LᵀL (which is 4x4, so neatly decomposes into four 2x2 matrices) with optimizations
            // presented in Ingemarsson, C and Gustafsson O, 2015.
            // P = [A  B]
            //     [Bᵀ D]
            // C = P⁻¹ = [E  F]
            //           [Fᵀ H]
            // V = A⁻¹ (A is symmetric, therefore V is also symmetric, so we only need to compute the upper triangular part)
            const detA = 1 / (P11 * P22 - P12 * P12);
            const V11 = P22 * detA;
            const V12 = -P12 * detA;
            const V22 = P11 * detA;
            // X = VB
            const X11 = V11 * P13 + V12 * P23;
            const X12 = V11 * P14 + V12 * P24;
            const X21 = V12 * P13 + V22 * P23;
            const X22 = V12 * P14 + V22 * P24;
            // H = (D - BᵀX)⁻¹ (H and D are symmetric, which means BᵀX is also symmetric)
            const Hi11 = P33 - (P13 * X11 + P23 * X21);
            const Hi12 = P34 - (P13 * X12 + P23 * X22);
            const Hi22 = P44 - (P14 * X12 + P24 * X22);
            const detHi = 1 / (Hi11 * Hi22 - Hi12 * Hi12);
            const H11 = Hi22 * detHi;
            const H12 = -Hi12 * detHi;
            const H22 = Hi11 * detHi;
            // Z = XH, F = -Z
            const Z11 = X11 * H11 + X12 * H12;
            const Z12 = X11 * H12 + X12 * H22;
            const Z21 = X21 * H11 + X22 * H12;
            const Z22 = X21 * H12 + X22 * H22;
            // E = V + ZXᵀ (E is symmetric, so we only need to compute the upper triangular part)
            const E11 = V11 + Z11 * X11 + Z12 * X12;
            const E12 = V12 + Z11 * X21 + Z12 * X22;
            const E22 = V22 + Z21 * X21 + Z22 * X22;
            out[0][0] = E11;
            out[0][1] = E12;
            out[0][2] = -Z11;
            out[0][3] = -Z12;
            out[1][0] = E12; // E is symmetric, so E21 = E12
            out[1][1] = E22;
            out[1][2] = -Z21;
            out[1][3] = -Z22;
            out[2][0] = -Z11;
            out[2][1] = -Z21;
            out[2][2] = H11;
            out[2][3] = H12;
            out[3][0] = -Z12;
            out[3][1] = -Z22;
            out[3][2] = H12; // H is symmetric, so H21 = H12
            out[3][3] = H22;
            return out;
        }
        /**
         * Calculates the transpose of the `S` matrix in the downdate satellite selection algorithm for a satellite
         * constellation. The index of a satellite's corresponding row in the `Sᵀ` matrix matches the index of its position
         * vector in the provided line-of-sight position matrix.
         * @param los The line-of-sight position matrix for the satellite constellation.
         * @param covar The position-covariance matrix for the satellite constellation.
         * @param out The matrix to which to write the result.
         * @returns The transpose of the `S` matrix in the downdate satellite selection algorithm for the specified satellite
         * constellation.
         */
        static calculateDowndateSTranspose(los, covar, out) {
            for (let i = 0; i < los.length; i++) {
                for (let j = 0; j < 4; j++) {
                    out[i][j] = 0;
                    for (let k = 0; k < 4; k++) {
                        out[i][j] += los[i][k] * covar[k][j];
                    }
                }
            }
            return out;
        }
        /**
         * Calculates the diagonal of the `P` matrix in the downdate satellite selection algorithm for a satellite
         * constellation.
         * @param los The line-of-sight position matrix for the satellite constellation.
         * @param sTranspose The transpose of the `S` matrix in the downdate satellite selection algorithm for the satellite
         * constellation.
         * @param out The vector to which to write the result.
         * @returns The diagonal of the `P` matrix in the downdate satellite selection algorithm for the specified satellite
         * constellation.
         */
        static calculateDowndatePDiag(los, sTranspose, out) {
            out.fill(1);
            for (let i = 0; i < los.length; i++) {
                for (let j = 0; j < 4; j++) {
                    out[i] -= sTranspose[i][j] * los[i][j];
                }
            }
            return out;
        }
        /**
         * Calculates the costs of removing each satellite from a constellation. The cost of removing a satellite is defined
         * as the amount by which `PDOP²` will increase when the satellite is removed relative to the full constellation. The
         * index of a satellite's cost in the returned array matches the index of the satellite's corresponding row in the
         * provided `Sᵀ` matrix.
         * @param sTranspose The transpose of the `S` matrix in the downdate satellite selection algorithm for the satellite
         * constellation.
         * @param pDiag The diagonal of the `P` matrix in the downdate satellite selection algorithm for the satellite
         * constellation.
         * @param out The array to which to write the results.
         * @returns The costs of removing each satellite from a constellation.
         */
        static calculateSatelliteCosts(sTranspose, pDiag, out) {
            out.length = sTranspose.length;
            for (let i = 0; i < sTranspose.length; i++) {
                out[i] = (sTranspose[i][0] * sTranspose[i][0] + sTranspose[i][1] * sTranspose[i][1] + sTranspose[i][2] * sTranspose[i][2]) / pDiag[i];
            }
            return out;
        }
    }
    GPSSatComputer.EPHEMERIS_COLLECTED_SATELLITE_STATES = new Set([exports.GPSSatelliteState.DataCollected, exports.GPSSatelliteState.InUse, exports.GPSSatelliteState.InUseDiffApplied]);
    GPSSatComputer.inUseSatelliteFilter = (sat) => {
        const state = sat.state.get();
        return state === exports.GPSSatelliteState.InUse || state === exports.GPSSatelliteState.InUseDiffApplied;
    };
    GPSSatComputer.readySatelliteFilter = (sat) => {
        const state = sat.state.get();
        return state === exports.GPSSatelliteState.DataCollected || state === exports.GPSSatelliteState.InUse || state === exports.GPSSatelliteState.InUseDiffApplied;
    };
    GPSSatComputer.createVec4 = () => new Float64Array(4);
    GPSSatComputer.COLLECTING_DATA_SATELLITE_STATES = new Set([
        exports.GPSSatelliteState.Acquired,
        exports.GPSSatelliteState.DataCollected,
        exports.GPSSatelliteState.InUse,
        exports.GPSSatelliteState.InUseDiffApplied
    ]);
    GPSSatComputer.covarMultiplyFuncs = [
        [0, 1, 2, 3].map(col => (sum, vec) => sum + vec[0] * vec[col]),
        [1, 2, 3].map(col => (sum, vec) => sum + vec[1] * vec[col]),
        [2, 3].map(col => (sum, vec) => sum + vec[2] * vec[col])
    ];
    /**
     * A tracked GPS satellite.
     */
    class GPSSatellite {
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The most recent simulation time at which this satellite's ephemeris was downloaded, as a Javascript timestamp, or
         * `undefined` if this satellite's ephemeris has not yet been downloaded.
         */
        get lastEphemerisTime() {
            return this._lastEphemerisTime;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The most recent simulation time at which this satellite was confirmed to be unreachable, as a Javascript
         * timestamp, or `undefined` if this satellite has not been confirmed to be unreachable.
         */
        get lastUnreachableTime() {
            return this._lastUnreachableTime;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether SBAS differential correction data have been downloaded from this satellite. */
        get areDiffCorrectionsDownloaded() {
            return this._areDiffCorrectionsDownloaded;
        }
        /**
         * Creates an instance of a GPSSatellite.
         * @param prn The GPS PRN number for this satellite.
         * @param sbasGroup Whether or not this satellite is a SBAS satellite.
         * @param ephemeris The ephemeris data to use for position calculation.
         * @param timingOptions Options with which to configure the timing of this satellite's state changes.
         */
        constructor(prn, sbasGroup, ephemeris, timingOptions) {
            this.prn = prn;
            this.sbasGroup = sbasGroup;
            this.ephemeris = ephemeris;
            this.timingOptions = timingOptions;
            this.vec3Cache = [new Float64Array(3), new Float64Array(3), new Float64Array(3), new Float64Array(3), new Float64Array(3)];
            /** The current satellite state. */
            this.state = Subject.create(exports.GPSSatelliteState.None);
            /** The current satellite position, in zenith angle radians and hour angle radians. */
            this.position = Vec2Subject.create(new Float64Array(2));
            /** The current satellite position, in cartesian coordinates. */
            this.positionCartesian = Vec3Subject.create(new Float64Array(3));
            /** The current satellite signal strength. */
            this.signalStrength = Subject.create(0);
            this.isTracked = false;
            this.hasComputedPosition = false;
            this._lastEphemerisTime = undefined;
            this._lastUnreachableTime = undefined;
            this._areDiffCorrectionsDownloaded = false;
            this.timeSpentAcquiring = undefined;
            this.timeToAcquire = undefined;
            this.timeToDownloadEphemeris = undefined;
            this.timeToDownloadCorrections = undefined;
        }
        /**
         * Computes the current satellite positions given the loaded ephemeris data.
         * @param simTime The current simulator time, in milliseconds UNIX epoch
         */
        computeSatellitePositions(simTime) {
            const record = this.ephemeris;
            if (record !== undefined) {
                const mu = 3.986005e14; //WGS84 gravitational constant for GPS user (meters3/sec2)
                const omegae_dot = 7.2921151467e-5; //WGS84 earth rotation rate (rad/sec)
                // Restore semi-major axis
                const a = record.sqrtA * record.sqrtA;
                // Computed mean motion
                const n0 = Math.sqrt(mu / (a * a * a));
                // Time from ephemeris reference epoch
                const now = simTime / 1000;
                const t = (now - (86400 * 3) + 1735) % 604800;
                let tk = t - record.toeTimeEphemeris;
                if (tk > 302400) {
                    tk -= 604800;
                }
                else if (tk < -302400) {
                    tk += 604800;
                }
                // Corrected mean motion
                const n = n0 + record.deltaN;
                // Mean anomaly
                const M = record.m0 + n * tk;
                // Initial guess of eccentric anomaly
                let E = M;
                let E_old;
                let dE;
                // Iterative computation of eccentric anomaly
                for (let i = 1; i < 20; i++) {
                    E_old = E;
                    E = M + record.eEccentricity * Math.sin(E);
                    dE = E - E_old % (2.0 * Math.PI);
                    if (Math.abs(dE) < 1e-12) {
                        // Necessary precision is reached, exit from the loop
                        break;
                    }
                }
                const sek = Math.sin(E);
                const cek = Math.cos(E);
                const OneMinusecosE = 1.0 - record.eEccentricity * cek;
                const sq1e2 = Math.sqrt(1.0 - record.eEccentricity * record.eEccentricity);
                // Compute the true anomaly
                const tmp_Y = sq1e2 * sek;
                const tmp_X = cek - record.eEccentricity;
                const nu = Math.atan2(tmp_Y, tmp_X);
                // Compute angle phi (argument of Latitude)
                const phi = nu + record.omegaS;
                // Reduce phi to between 0 and 2*pi rad
                const s2pk = Math.sin(2.0 * phi);
                const c2pk = Math.cos(2.0 * phi);
                // Correct argument of latitude
                const u = phi + record.cuc * c2pk + record.cus * s2pk;
                const suk = Math.sin(u);
                const cuk = Math.cos(u);
                // Correct radius
                const r = a * OneMinusecosE + record.crc * c2pk + record.crs * s2pk;
                // Correct inclination
                const i = record.i0 + record.idot * tk + record.cic * c2pk + record.cis * s2pk;
                const sik = Math.sin(i);
                const cik = Math.cos(i);
                // Compute the angle between the ascending node and the Greenwich meridian
                const Omega_dot = record.omegaLDot - omegae_dot;
                const Omega = record.omegaL + Omega_dot * tk - omegae_dot * record.toeTimeEphemeris;
                const sok = Math.sin(Omega);
                const cok = Math.cos(Omega);
                //Compute satellite coordinates in Earth-fixed coordinates
                const xprime = r * cuk;
                const yprime = r * suk;
                const x = xprime * cok - yprime * cik * sok;
                const y = xprime * sok + yprime * cik * cok;
                const z = yprime * sik;
                this.positionCartesian.set(UnitType.METER.convertTo(x, UnitType.GA_RADIAN), UnitType.METER.convertTo(y, UnitType.GA_RADIAN), UnitType.METER.convertTo(z, UnitType.GA_RADIAN));
            }
        }
        /**
         * Applies a projection to the satellite cartesian coordinates to convert to zenith and hour angles.
         * @param ppos The current plane position.
         * @param altitude The current plane altitude in meters.
         */
        applyProjection(ppos, altitude) {
            const satPos = this.positionCartesian.get();
            const altRadians = UnitType.METER.convertTo(altitude, UnitType.GA_RADIAN);
            const pposCartesian = Vec3Math.multScalar(ppos.toCartesian(this.vec3Cache[0]), 1 + altRadians, this.vec3Cache[0]);
            const delta = Vec3Math.normalize(Vec3Math.sub(satPos, pposCartesian, this.vec3Cache[1]), this.vec3Cache[1]);
            const zenithAngle = Math.acos(Vec3Math.dot(delta, Vec3Math.normalize(pposCartesian, this.vec3Cache[2])));
            const satPos0 = Vec3Math.normalize(satPos, this.vec3Cache[1]);
            const northPole = Vec3Math.set(0, 0, 1, this.vec3Cache[2]);
            if (Math.abs(zenithAngle) < 1e-8 || Math.abs(zenithAngle - 180) < 1e-8) {
                this.position.set(zenithAngle, 0);
            }
            else {
                const A = Vec3Math.normalize(Vec3Math.cross(pposCartesian, northPole, this.vec3Cache[3]), this.vec3Cache[3]);
                const B = Vec3Math.normalize(Vec3Math.cross(pposCartesian, satPos0, this.vec3Cache[4]), this.vec3Cache[4]);
                const signBz = B[2] >= 0 ? 1 : -1;
                const hourAngle = Math.acos(Vec3Math.dot(A, B)) * signBz;
                this.position.set(zenithAngle, -hourAngle);
            }
            this.hasComputedPosition = true;
        }
        /**
         * Calculates the current signal strength.
         * @param invMaxZenithAngle The inverse of the maximum zenith angle at which a satellite can still have line of sight, in radians.
         */
        calculateSignalStrength(invMaxZenithAngle) {
            if (this.hasComputedPosition) {
                const signalStrength = Math.max(0, 1 - (this.position.get()[0] * invMaxZenithAngle));
                this.signalStrength.set(signalStrength);
            }
        }
        /**
         * Calculates the horizon zenith angle.
         * @param altitude The altitude, in meters.
         * @returns The calculated horizon zenith angle based on the current altitude.
         */
        static calcHorizonAngle(altitude) {
            return Math.acos(6378100 / (6378100 + Math.max(altitude, 0)));
        }
        /**
         * Checks whether this satellite's cached ephemeris data is valid at a given simulation time.
         * @param simTime The simulation time at which to check for ephemeris validity, as a Javascript timestamp.
         * @returns Whether this satellite's cached ephemeris data is valid at the specified simulation time.
         */
        isCachedEphemerisValid(simTime) {
            return this._lastEphemerisTime !== undefined && Math.abs(simTime - this._lastEphemerisTime) < this.timingOptions.ephemerisExpireTime;
        }
        /**
         * Erases this satellite's cached ephemeris data.
         */
        eraseCachedEphemeris() {
            this._lastEphemerisTime = undefined;
        }
        /**
         * Sets whether this satellite is being tracked by a receiver channel.
         * @param tracked Whether this satellite is being tracked by a receiver channel.
         */
        setTracked(tracked) {
            if (this.isTracked === tracked) {
                return;
            }
            this.isTracked = tracked;
            this.timeSpentAcquiring = undefined;
            this.timeToAcquire = undefined;
            this.timeToDownloadEphemeris = undefined;
            this._areDiffCorrectionsDownloaded = false;
            this.timeToDownloadCorrections = undefined;
            if (tracked || this.state.get() !== exports.GPSSatelliteState.Unreachable) {
                this.state.set(exports.GPSSatelliteState.None);
            }
        }
        /**
         * Updates the state of the satellite.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @param deltaTime The amount of sim time that has elapsed since the last update, in milliseconds.
         * @param distanceFromLastKnownPos The distance, in great-arc radians, from the airplane's current actual position to
         * its last known position.
         * @param forceAcquireAndUse Whether to force this satellite to the highest possible use state
         * ({@link GPSSatelliteState.DataCollected}) if signal strength is sufficient.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        updateState(simTime, deltaTime, distanceFromLastKnownPos, forceAcquireAndUse) {
            const stateChanged = this.isTracked
                ? this.updateStateTracked(simTime, deltaTime, distanceFromLastKnownPos, forceAcquireAndUse)
                : this.updateStateUntracked(simTime);
            switch (this.state.get()) {
                case exports.GPSSatelliteState.Unreachable:
                    if (this.isTracked) {
                        this._lastUnreachableTime = simTime;
                    }
                    break;
                case exports.GPSSatelliteState.DataCollected:
                case exports.GPSSatelliteState.InUse:
                case exports.GPSSatelliteState.InUseDiffApplied:
                    this._lastEphemerisTime = simTime;
                    break;
            }
            return stateChanged;
        }
        /**
         * Updates the state of the satellite while it is being tracked.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @param deltaTime The amount of sim time that has elapsed since the last update, in milliseconds.
         * @param distanceFromLastKnownPos The distance, in great-arc radians, from the airplane's current actual position to
         * its last known position.
         * @param forceAcquireAndUse Whether to force this satellite to the highest possible use state
         * ({@link GPSSatelliteState.DataCollected}) if signal strength is sufficient.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        updateStateTracked(simTime, deltaTime, distanceFromLastKnownPos, forceAcquireAndUse) {
            const reachable = this.signalStrength.get() > 0.05;
            if (forceAcquireAndUse) {
                const state = this.state.get();
                if (reachable) {
                    if (this.sbasGroup !== undefined) {
                        this._areDiffCorrectionsDownloaded = true;
                        this.timeToDownloadCorrections = undefined;
                    }
                    if (state !== exports.GPSSatelliteState.DataCollected) {
                        this.timeSpentAcquiring = undefined;
                        this.timeToAcquire = undefined;
                        this.timeToDownloadEphemeris = undefined;
                        this.state.set(exports.GPSSatelliteState.DataCollected);
                        return true;
                    }
                }
                else {
                    if (state !== exports.GPSSatelliteState.Unreachable) {
                        this.timeSpentAcquiring = undefined;
                        this.timeToAcquire = undefined;
                        this.timeToDownloadEphemeris = undefined;
                        this._areDiffCorrectionsDownloaded = false;
                        this.timeToDownloadCorrections = undefined;
                        this.state.set(exports.GPSSatelliteState.Unreachable);
                        return true;
                    }
                }
            }
            else {
                switch (this.state.get()) {
                    case exports.GPSSatelliteState.None:
                        if (this.timeSpentAcquiring === undefined) {
                            this.timeSpentAcquiring = 0;
                        }
                        this.timeSpentAcquiring += deltaTime;
                        if (reachable) {
                            if (this.timeToAcquire === undefined) {
                                this.timeToAcquire = distanceFromLastKnownPos < 5.80734e-4 /* 2 nautical miles */ && this.isCachedEphemerisValid(simTime)
                                    ? this.timingOptions.acquisitionTimeWithEphemeris + (Math.random() - 0.5) * this.timingOptions.acquisitionTimeRangeWithEphemeris
                                    : this.timingOptions.acquisitionTime + (Math.random() - 0.5) * this.timingOptions.acquisitionTimeRange;
                            }
                            this.timeToAcquire -= deltaTime;
                            if (this.timeToAcquire <= 0) {
                                this.timeSpentAcquiring = undefined;
                                this.timeToAcquire = undefined;
                                // If we have valid cached ephemeris data for this satellite, then we can use the cached data for
                                // calculating position solutions immediately instead of having to wait to download new ephemeris data.
                                if (this.isCachedEphemerisValid(simTime)) {
                                    this.state.set(exports.GPSSatelliteState.DataCollected);
                                }
                                else {
                                    this.state.set(exports.GPSSatelliteState.Acquired);
                                }
                                return true;
                            }
                        }
                        else {
                            this.timeToAcquire = undefined;
                            if (this.timeSpentAcquiring >= this.timingOptions.acquisitionTimeout) {
                                this.timeSpentAcquiring = undefined;
                                this.state.set(exports.GPSSatelliteState.Unreachable);
                                return true;
                            }
                        }
                        break;
                    case exports.GPSSatelliteState.Unreachable:
                        if (this._lastUnreachableTime === undefined) {
                            this._lastUnreachableTime = simTime;
                        }
                        else if (Math.abs(simTime - this._lastUnreachableTime) >= this.timingOptions.unreachableExpireTime) {
                            this._lastUnreachableTime = undefined;
                            this.state.set(exports.GPSSatelliteState.None);
                            return true;
                        }
                        break;
                    case exports.GPSSatelliteState.Acquired:
                        if (!reachable) {
                            this.timeToDownloadEphemeris = undefined;
                            this._areDiffCorrectionsDownloaded = false;
                            this.timeToDownloadCorrections = undefined;
                            this.state.set(exports.GPSSatelliteState.None);
                            return true;
                        }
                        else {
                            if (this.timeToDownloadEphemeris === undefined) {
                                this.timeToDownloadEphemeris = this.sbasGroup === undefined
                                    ? this.timingOptions.ephemerisDownloadTime
                                    : this.timingOptions.sbasEphemerisDownloadTime + (Math.random() - 0.5) * this.timingOptions.sbasEphemerisDownloadTimeRange;
                            }
                            this.timeToDownloadEphemeris -= deltaTime;
                            this.updateSbasCorrectionsDownload(deltaTime);
                            if (this.timeToDownloadEphemeris <= 0) {
                                this.timeToDownloadEphemeris = undefined;
                                this.state.set(exports.GPSSatelliteState.DataCollected);
                                return true;
                            }
                        }
                        break;
                    case exports.GPSSatelliteState.DataCollected:
                        if (!reachable) {
                            this._areDiffCorrectionsDownloaded = false;
                            this.timeToDownloadCorrections = undefined;
                            this.state.set(exports.GPSSatelliteState.None);
                            return true;
                        }
                        else {
                            this.updateSbasCorrectionsDownload(deltaTime);
                        }
                        break;
                    case exports.GPSSatelliteState.InUse:
                        if (!reachable) {
                            this._areDiffCorrectionsDownloaded = false;
                            this.timeToDownloadCorrections = undefined;
                            this.state.set(exports.GPSSatelliteState.None);
                            return true;
                        }
                        else {
                            this.updateSbasCorrectionsDownload(deltaTime);
                        }
                        break;
                    case exports.GPSSatelliteState.InUseDiffApplied:
                        if (!reachable) {
                            this._areDiffCorrectionsDownloaded = false;
                            this.timeToDownloadCorrections = undefined;
                            this.state.set(exports.GPSSatelliteState.None);
                            return true;
                        }
                        else {
                            this.updateSbasCorrectionsDownload(deltaTime);
                        }
                        break;
                }
            }
            return false;
        }
        /**
         * Updates the download state of SBAS differential corrections from this satellite.
         * @param deltaTime The amount of sim time that has elapsed since the last update, in milliseconds.
         */
        updateSbasCorrectionsDownload(deltaTime) {
            if (this.sbasGroup === undefined || this._areDiffCorrectionsDownloaded) {
                return;
            }
            if (this.timeToDownloadCorrections === undefined) {
                this.timeToDownloadCorrections = this.timingOptions.sbasCorrectionDownloadTime + (Math.random() - 0.5) * this.timingOptions.sbasCorrectionDownloadTimeRange;
            }
            this.timeToDownloadCorrections -= deltaTime;
            if (this.timeToDownloadCorrections <= 0) {
                this._areDiffCorrectionsDownloaded = true;
                this.timeToDownloadCorrections = undefined;
            }
        }
        /**
         * Updates the state of the satellite while it is not being tracked.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        updateStateUntracked(simTime) {
            if (this.state.get() === exports.GPSSatelliteState.Unreachable) {
                if (this._lastUnreachableTime === undefined) {
                    this._lastUnreachableTime = simTime;
                }
                else if (Math.abs(simTime - this._lastUnreachableTime) >= this.timingOptions.unreachableExpireTime) {
                    this._lastUnreachableTime = undefined;
                    this.state.set(exports.GPSSatelliteState.None);
                    return true;
                }
            }
            return false;
        }
        /**
         * Forces an update of this satellite's state to a specific value.
         * @param simTime The current simulation time, as a Javascript timestamp.
         * @param state The state to which to update this satellite. Defaults to this satellite's current state.
         * @param areDiffCorrectionsDownloaded Whether to force differential corrections to be downloaded. Defaults to the
         * satellite's current differential corrections download state.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        forceUpdateState(simTime, state = this.state.get(), areDiffCorrectionsDownloaded = this._areDiffCorrectionsDownloaded) {
            switch (state) {
                case exports.GPSSatelliteState.Unreachable:
                    this.timeSpentAcquiring = undefined;
                    this.timeToAcquire = undefined;
                    if (this.isTracked) {
                        this._lastUnreachableTime = simTime;
                    }
                // fallthrough
                case exports.GPSSatelliteState.None:
                    this.timeToDownloadEphemeris = undefined;
                    this._areDiffCorrectionsDownloaded = false;
                    this.timeToDownloadCorrections = undefined;
                    break;
                case exports.GPSSatelliteState.DataCollected:
                case exports.GPSSatelliteState.InUse:
                case exports.GPSSatelliteState.InUseDiffApplied:
                    this.timeToDownloadEphemeris = undefined;
                    this._lastEphemerisTime = simTime;
                    break;
            }
            this._areDiffCorrectionsDownloaded = this.sbasGroup !== undefined && areDiffCorrectionsDownloaded;
            if (this.state.get() !== state) {
                this.state.set(state);
                return true;
            }
            else {
                return false;
            }
        }
        /**
         * Updates whether this satellite is being used to calculate a position solution.
         * @param inUse Whether the satellite is being used to calculate a position solution.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        updateInUse(inUse) {
            if (inUse) {
                if (this.state.get() === exports.GPSSatelliteState.DataCollected) {
                    this.state.set(exports.GPSSatelliteState.InUse);
                    return true;
                }
            }
            else {
                switch (this.state.get()) {
                    case exports.GPSSatelliteState.InUse:
                    case exports.GPSSatelliteState.InUseDiffApplied:
                        this.state.set(exports.GPSSatelliteState.DataCollected);
                        return true;
                }
            }
            return false;
        }
        /**
         * Updates whether differential corrections are applied to this satellite's ranging data when they are used to
         * calculate a position solution.
         * @param apply Whether differential corrections are applied.
         * @returns Whether this satellite's state changed as a result of the update.
         */
        updateDiffCorrectionsApplied(apply) {
            switch (this.state.get()) {
                case exports.GPSSatelliteState.InUse:
                    if (apply) {
                        this.state.set(exports.GPSSatelliteState.InUseDiffApplied);
                        return true;
                    }
                    break;
                case exports.GPSSatelliteState.InUseDiffApplied:
                    if (!apply) {
                        this.state.set(exports.GPSSatelliteState.InUse);
                        return true;
                    }
                    break;
            }
            return false;
        }
    }

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/baseinstrument" />
    /**
     * A publisher for VCockpit BaseInstrument events.
     */
    class BaseInstrumentPublisher extends BasePublisher {
        /**
         * Creates an instance of BasePublisher.
         * @param instrument The BaseInstrument instance.
         * @param bus The common event bus.
         * @param pacer An optional pacer to control the rate of publishing.
         */
        constructor(instrument, bus, pacer = undefined) {
            super(bus, pacer);
            this.instrument = instrument;
            this.lastGameState = undefined;
            this.lastIsPowered = undefined;
            this.lastScreenState = undefined;
            this.hasFlightStarted = false;
            instrument.addEventListener('mouseleave', (e) => {
                this.publish('vc_mouse_leave', e, false, false);
            });
            instrument.addEventListener('mouseenter', (e) => {
                this.publish('vc_mouse_enter', e, false, false);
            });
        }
        /** @inheritdoc */
        startPublish() {
            super.startPublish();
        }
        /** @inheritdoc */
        stopPublish() {
            super.stopPublish();
        }
        /** @inheritdoc */
        onUpdate() {
            if (!this.isPublishing()) {
                return;
            }
            this.updateFromGameState(this.instrument.getGameState());
            this.updateFromPowered(this.instrument.isStarted); // Big hack here since there is no other way to get the isStarted state from BaseInstrument
            this.updateFromScreenState(this.instrument.screenState); // Another big hack
        }
        /**
         * Updates this publisher from the current game state.
         * @param gameState The current game state.
         */
        updateFromGameState(gameState) {
            if (this.lastGameState === gameState) {
                return;
            }
            this.lastGameState = gameState;
            this.publish('vc_game_state', gameState);
            if (!this.hasFlightStarted && gameState === GameState.ingame) {
                this.publish('vc_flight_start', true);
            }
        }
        /**
         * Updates this publisher from the current powered state.
         * @param isPowered The current powered state.
         */
        updateFromPowered(isPowered) {
            if (this.lastIsPowered === isPowered) {
                return;
            }
            this.lastIsPowered = isPowered;
            this.publish('vc_powered', isPowered);
        }
        /**
         * Updates this publisher from the current screen state.
         * @param screenState The current screen state.
         */
        updateFromScreenState(screenState) {
            if (this.lastScreenState === screenState) {
                return;
            }
            const lastScreenState = this.lastScreenState;
            this.lastScreenState = screenState;
            this.publish('vc_screen_state', { current: screenState, previous: lastScreenState });
        }
    }

    /**
     * A publisher for data from the Modular Liquid Dropping system in the simulator.
     */
    class LiquidDropPublisher extends SimVarPublisher {
        /**
         * Create an LiquidDropPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer) {
            super([
                ['liquid_drop_system_max_volume', { name: 'LIQUID DROPPING TANK TOTAL CAPACITY VOLUME', type: exports.SimVarValueType.GAL }],
                ['liquid_drop_system_max_weight', { name: 'LIQUID DROPPING TANK TOTAL CAPACITY', type: exports.SimVarValueType.Pounds }],
                ['liquid_drop_system_volume', { name: 'LIQUID DROPPING TANK TOTAL CURRENT VOLUME', type: exports.SimVarValueType.GAL }],
                ['liquid_drop_system_weight', { name: 'LIQUID DROPPING TANK TOTAL WEIGHT', type: exports.SimVarValueType.Pounds }],
                ['liquid_drop_system_drop_flow_rate', { name: 'LIQUID DROPPING TOTAL DROPPED FLOW', type: exports.SimVarValueType.PPH }],
                ['liquid_drop_system_scoop_flow_rate', { name: 'LIQUID DROPPING TOTAL SCOOPED FLOW', type: exports.SimVarValueType.PPH }],
                ['liquid_drop_tank_max_volume', { name: 'LIQUID DROPPING TANK CAPACITY VOLUME:#index#', type: exports.SimVarValueType.GAL, indexed: true, defaultIndex: null }],
                ['liquid_drop_tank_max_weight', { name: 'LIQUID DROPPING TANK CAPACITY:#index#', type: exports.SimVarValueType.Pounds, indexed: true, defaultIndex: null }],
                ['liquid_drop_tank_volume', { name: 'LIQUID DROPPING TANK CURRENT VOLUME:#index#', type: exports.SimVarValueType.GAL, indexed: true, defaultIndex: null }],
                ['liquid_drop_tank_weight', { name: 'LIQUID DROPPING TANK WEIGHT:#index#', type: exports.SimVarValueType.Pounds, indexed: true, defaultIndex: null }],
                ['liquid_drop_door_flow_rate', { name: 'LIQUID DROPPING DOOR FLOW:#index#', type: exports.SimVarValueType.PPH, indexed: true, defaultIndex: null }],
                ['liquid_drop_door_open_target_pct', { name: 'LIQUID DROPPING DOOR OPEN TARGET:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
                ['liquid_drop_door_open_pct', { name: 'LIQUID DROPPING DOOR OPEN VALUE:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
                ['liquid_drop_scoop_flow_rate', { name: 'LIQUID DROPPING SCOOP FLOW:#index#', type: exports.SimVarValueType.PPH, indexed: true, defaultIndex: null }],
                ['liquid_drop_scoop_open_target_pct', { name: 'LIQUID DROPPING SCOOP OPEN TARGET:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
                ['liquid_drop_scoop_open_pct', { name: 'LIQUID DROPPING SCOOP OPEN VALUE:#index#', type: exports.SimVarValueType.Percent, indexed: true, defaultIndex: null }],
            ], bus, pacer);
        }
    }

    /** Minimums Modes */
    exports.MinimumsMode = void 0;
    (function (MinimumsMode) {
        MinimumsMode[MinimumsMode["OFF"] = 0] = "OFF";
        MinimumsMode[MinimumsMode["BARO"] = 1] = "BARO";
        MinimumsMode[MinimumsMode["RA"] = 2] = "RA";
        MinimumsMode[MinimumsMode["TEMP_COMP_BARO"] = 3] = "TEMP_COMP_BARO";
    })(exports.MinimumsMode || (exports.MinimumsMode = {}));
    /** A publisher for minimums simvar events. */
    class MinimumsSimVarPublisher extends SimVarPublisher {
        /**
         * @inheritdoc
         */
        constructor(bus) {
            super(MinimumsSimVarPublisher.simvars, bus);
        }
    }
    MinimumsSimVarPublisher.simvars = new Map([
        ['decision_height_feet', { name: 'DECISION HEIGHT', type: exports.SimVarValueType.Feet }],
        ['decision_altitude_feet', { name: 'DECISION ALTITUDE MSL', type: exports.SimVarValueType.Feet }],
        ['minimums_mode', { name: 'L:WT_MINIMUMS_MODE', type: exports.SimVarValueType.Number }]
    ]);
    /**
     * A class that manages decision height and altitude data and events.
     */
    class MinimumsManager {
        /**
         * Create a MinimumsManager
         * @param bus The event bus
         */
        constructor(bus) {
            this.currentDH = 0;
            this.currentDA = 0;
            this.daDistanceUnit = UnitType.FOOT;
            this.dhDistanceUnit = UnitType.FOOT;
            this.bus = bus;
            this.controlSubscriber = bus.getSubscriber();
            // Initialize both simvars to 0.
            SimVar.SetSimVarValue('K:SET_DECISION_HEIGHT', 'number', 0);
            SimVar.SetSimVarValue('K:SET_DECISION_ALTITUDE_MSL', 'number', 0);
            KeyEventManager.getManager(bus).then(manager => {
                manager.interceptKey('INCREASE_DECISION_HEIGHT', false);
                manager.interceptKey('DECREASE_DECISION_HEIGHT', false);
                manager.interceptKey('INCREASE_DECISION_ALTITUDE_MSL', false);
                manager.interceptKey('DECREASE_DECISION_ALTITUDE_MSL', false);
            });
            this.controlSubscriber.on('set_decision_height_feet').handle((dh) => {
                SimVar.SetSimVarValue('K:SET_DECISION_HEIGHT', exports.SimVarValueType.Number, dh);
            });
            this.controlSubscriber.on('set_decision_altitude_feet').handle((da) => {
                SimVar.SetSimVarValue('K:SET_DECISION_ALTITUDE_MSL', exports.SimVarValueType.Number, da);
            });
            this.controlSubscriber.on('set_minimums_mode').handle((mode) => {
                SimVar.SetSimVarValue('L:WT_MINIMUMS_MODE', exports.SimVarValueType.Number, mode);
            });
            this.controlSubscriber.on('set_dh_distance_unit').handle((unit) => {
                this.dhDistanceUnit = unit == 'meters' ? UnitType.METER : UnitType.FOOT;
            });
            this.controlSubscriber.on('set_da_distance_unit').handle((unit) => {
                this.daDistanceUnit = unit == 'meters' ? UnitType.METER : UnitType.FOOT;
            });
            const sub = this.bus.getSubscriber();
            sub.on('key_intercept').handle((evt) => {
                let simvar;
                let curVal;
                let direction = 'up';
                let unit;
                if (evt.value0 !== undefined) {
                    switch (evt.key) {
                        case 'DECREASE_DECISION_HEIGHT':
                            direction = 'down';
                        // eslint-disable-next-line no-fallthrough
                        case 'INCREASE_DECISION_HEIGHT':
                            simvar = 'K:SET_DECISION_HEIGHT';
                            unit = this.dhDistanceUnit;
                            curVal = this.currentDH;
                            break;
                        case 'DECREASE_DECISION_ALTITUDE_MSL':
                            direction = 'down';
                        // eslint-disable-next-line no-fallthrough
                        case 'INCREASE_DECISION_ALTITUDE_MSL':
                            simvar = 'K:SET_DECISION_ALTITUDE_MSL';
                            unit = this.daDistanceUnit;
                            curVal = this.currentDA;
                            break;
                    }
                    if (simvar !== undefined && curVal !== undefined && unit !== undefined) {
                        // There is one flaw in this logic, but I'm not sure what can be done about
                        // it.  You can set the inc/dec amount via the K event in feet or meters.
                        // If your user preference unit is one, but the simvar call uses the other,
                        // we have now way of knowing  about it so will force a conversion that's not
                        // needed.This is a fairly minor flaw, but worth acknowledging until a
                        // workaround can be found.
                        const increment = unit.convertTo(evt.value0, UnitType.FOOT) * (direction == 'down' ? -1 : 1);
                        SimVar.SetSimVarValue(simvar, 'number', curVal + increment);
                    }
                }
            });
        }
    }

    // There are two main componints to the Nav/Com system currently, because we
    // can't rely on K events being processed synchronously.   Instead, we need
    // to run a listener which picks up frequency change events, in additon to a
    // setter that handles interavion events via H events.
    /**
     * A convenience class for creating a radio configuration set.
     *
     * Implementers should instantiate this and then populate the various maps
     * with the H events that their radio sends and which displays they affect.
     */
    class NavComConfig {
        constructor() {
            this.navSwitchEvents = new Map();
            this.navSelectorEvents = new Map();
            this.navWholeIncEvents = new Map();
            this.navWholeDecEvents = new Map();
            this.navFractionIncEvents = new Map();
            this.navFractionDecEvents = new Map();
            this.comSwitchEvents = new Map();
            this.comSelectorEvents = new Map();
            this.comWholeIncEvents = new Map();
            this.comWholeDecEvents = new Map();
            this.comFractionIncEvents = new Map();
            this.comFractionDecEvents = new Map();
            this.comVolumeIncEvents = new Map();
            this.comVolumeDecEvents = new Map();
            this.navVolumeIncEvents = new Map();
            this.navVolumeDecEvents = new Map();
        }
    }
    /**
     * Sends radio events from the nav/com controller to subscribers.
     */
    class NavComPublisher extends BasePublisher {
        /**
         * Creates a NavComPublisher
         * @param bus The event bus to publish to.
         * @param pacer An optional pace to use to control the rate of publishing.
         * @param sync Whether to use synced events.
         */
        constructor(bus, pacer, sync = true) {
            super(bus, pacer);
            this.sync = sync;
        }
        /**
         * Publish a radio state event.
         *
         * This sets the complete state of a radio for initialzation or resync.
         * @param radio The Radio data to publish.
         */
        publishRadioState(radio) {
            if (radio !== undefined) {
                super.publish('set_radio_state', radio, this.sync);
            }
        }
        /**
         * Publish a frequency change event.
         *
         * Unlike a radio state event, this just changes a specific frequency.
         * We provide this to avoid issues with potentially conflicting updates
         * if active and standby get updated quickly and we send a snapshot after
         * each.
         * @param radio The Radio to change.
         * @param bank The frequency bank to update.
         * @param frequency The new frequency to set.
         */
        publishFreqChange(radio, bank, frequency) {
            if (radio !== undefined) {
                super.publish('set_frequency', { radio: radio, bank: bank, frequency: frequency }, this.sync);
            }
        }
        /**
         * Publish the ident of the currently tuned station.
         * @param index The index number of the tuned radio.
         * @param ident The ident as a string.
         */
        publishIdent(index, ident) {
            super.publish('set_ident', { index: index, ident: ident }, this.sync);
        }
        /**
         * Publish the signal strength of the currently tuned station.
         * @param index The index number of the tuned radio.
         * @param strength The signal strength as a number.
         */
        publishSignalStrength(index, strength) {
            super.publish('set_signal_strength', strength, this.sync);
        }
        /**
         * Publish the ADF1 Active Frequency in Khz.
         * @param freq The active frequency in Khz.
         */
        publishAdfActiveFrequencySet(freq) {
            super.publish('adf_active_frequency_1', freq, false);
        }
        /**
         * Publish the ADF1 Standby Frequency in Khz.
         * @param freq The standby frequency in Khz.
         */
        publishAdfStandbyFrequencySet(freq) {
            super.publish('adf_standby_frequency_1', freq, false);
        }
    }
    /**
     * The core instrument that will drive all of a system's radios.
     *
     * Requires that the topics defined in {@link NavComEvents} are published to the event bus.
     */
    class NavComInstrument {
        /**
         * Create a NavComController.
         * @param bus The event bus to publish to.
         * @param config A NavComConfig object defining the radio configuration.
         * @param numNavRadios The number of nav radios in the system.
         * @param numComRadios The number of com radios in the system.
         * @param sync Whether to sync events or not, default true.
         */
        constructor(bus, config, numNavRadios, numComRadios, sync = true) {
            this.navRadios = new Array();
            this.comRadios = new Array();
            /**
             * Handle an hEvent.
             * @param hEvent The event that needs to be handled.
             */
            this.eventHandler = (hEvent) => {
                var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
                if (this.config !== undefined) {
                    // We can't use a switch statement here because of the need to retrieve
                    // the key from each map.  Sorry it's so ugly.
                    if ((_a = this.config.navSwitchEvents) === null || _a === void 0 ? void 0 : _a.has(hEvent)) {
                        this.swapFreqs(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_b = this.config.navSelectorEvents) === null || _b === void 0 ? void 0 : _b.has(hEvent)) {
                        this.swapSelection(this.navRadios);
                    }
                    else if ((_c = this.config.navWholeIncEvents) === null || _c === void 0 ? void 0 : _c.has(hEvent)) {
                        this.wholeInc(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_d = this.config.navWholeDecEvents) === null || _d === void 0 ? void 0 : _d.has(hEvent)) {
                        this.wholeDec(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_e = this.config.navFractionIncEvents) === null || _e === void 0 ? void 0 : _e.has(hEvent)) {
                        this.fractInc(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_f = this.config.navFractionDecEvents) === null || _f === void 0 ? void 0 : _f.has(hEvent)) {
                        this.fractDec(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_g = this.config.comSwitchEvents) === null || _g === void 0 ? void 0 : _g.has(hEvent)) {
                        this.swapFreqs(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_h = this.config.comSelectorEvents) === null || _h === void 0 ? void 0 : _h.has(hEvent)) {
                        this.swapSelection(this.comRadios);
                    }
                    else if ((_j = this.config.comWholeIncEvents) === null || _j === void 0 ? void 0 : _j.has(hEvent)) {
                        this.wholeInc(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_k = this.config.comWholeDecEvents) === null || _k === void 0 ? void 0 : _k.has(hEvent)) {
                        this.wholeDec(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_l = this.config.comFractionIncEvents) === null || _l === void 0 ? void 0 : _l.has(hEvent)) {
                        this.fractInc(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_m = this.config.comFractionDecEvents) === null || _m === void 0 ? void 0 : _m.has(hEvent)) {
                        this.fractDec(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_o = this.config.comVolumeIncEvents) === null || _o === void 0 ? void 0 : _o.has(hEvent)) {
                        this.volumeInc(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_p = this.config.comVolumeDecEvents) === null || _p === void 0 ? void 0 : _p.has(hEvent)) {
                        this.volumeDec(this.getSelectedRadio(this.comRadios));
                    }
                    else if ((_q = this.config.navVolumeIncEvents) === null || _q === void 0 ? void 0 : _q.has(hEvent)) {
                        this.volumeInc(this.getSelectedRadio(this.navRadios));
                    }
                    else if ((_r = this.config.navVolumeDecEvents) === null || _r === void 0 ? void 0 : _r.has(hEvent)) {
                        this.volumeDec(this.getSelectedRadio(this.navRadios));
                    }
                }
            };
            this.bus = bus;
            this.config = config;
            // Populate our radio arrays.
            for (let i = 1; i <= numNavRadios; i++) {
                this.navRadios.push({
                    index: i,
                    activeFrequency: 0,
                    ident: null,
                    signal: 0,
                    standbyFrequency: 0,
                    radioType: exports.RadioType.Nav,
                    selected: false
                });
            }
            for (let i = 1; i <= numComRadios; i++) {
                this.comRadios.push({
                    index: i,
                    activeFrequency: 0,
                    ident: null,
                    signal: 0,
                    standbyFrequency: 0,
                    radioType: exports.RadioType.Com,
                    selected: false
                });
            }
            // Create our publishers and subscribers.
            this.hevents = this.bus.getSubscriber();
            this.publisher = new NavComPublisher(bus, undefined, sync);
            this.simVarSubscriber = new EventSubscriber(this.bus);
            this.controlSubscriber = bus.getSubscriber();
        }
        /**
         * Initialize the instrument.
         */
        init() {
            // Start our two publishers.
            this.publisher.startPublish();
            // Set up our event handlers, for both H events and simvar updates.
            this.hevents.on('hEvent').handle(this.eventHandler);
            const navProcessorSubscriber = this.bus.getSubscriber();
            navProcessorSubscriber.on('cdi_select').handle(this.setActiveRadio.bind(this));
            this.controlSubscriber.on('publish_radio_states').handle(this.publishRadioStates.bind(this));
            this.controlSubscriber.on('standby_com_freq').handle(this.setStandbyFreq.bind(this, exports.RadioType.Com));
            this.controlSubscriber.on('com_spacing_set').handle(this.setComSpacing.bind(this));
            this.controlSubscriber.on('standby_nav_freq').handle(this.setStandbyFreq.bind(this, exports.RadioType.Nav));
            this.simVarSubscriber.on('nav_active_frequency_1').whenChangedBy(0.01).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Nav, 0, exports.FrequencyBank.Active, data);
            });
            this.simVarSubscriber.on('nav_standby_frequency_1').whenChangedBy(0.01).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Nav, 0, exports.FrequencyBank.Standby, data);
            });
            this.simVarSubscriber.on('nav_active_frequency_2').whenChangedBy(0.01).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Nav, 1, exports.FrequencyBank.Active, data);
            });
            this.simVarSubscriber.on('nav_standby_frequency_2').whenChangedBy(0.01).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Nav, 1, exports.FrequencyBank.Standby, data);
            });
            this.simVarSubscriber.on('com_active_frequency_1').whenChangedBy(0.001).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Com, 0, exports.FrequencyBank.Active, data);
            });
            this.simVarSubscriber.on('com_standby_frequency_1').whenChangedBy(0.001).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Com, 0, exports.FrequencyBank.Standby, data);
            });
            this.simVarSubscriber.on('com_active_frequency_2').whenChangedBy(0.001).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Com, 1, exports.FrequencyBank.Active, data);
            });
            this.simVarSubscriber.on('com_standby_frequency_2').whenChangedBy(0.001).handle((data) => {
                this.updateRadioFreqCb(exports.RadioType.Com, 1, exports.FrequencyBank.Standby, data);
            });
            this.simVarSubscriber.on('nav_ident_1').whenChanged().handle((data) => {
                this.navRadios[0].ident = data;
                this.publisher.publishIdent(1, data);
            });
            this.simVarSubscriber.on('nav_ident_2').whenChanged().handle((data) => {
                this.navRadios[1].ident = data;
                this.publisher.publishIdent(2, data);
            });
            this.simVarSubscriber.on('nav_signal_1').withPrecision(0).handle((data) => {
                this.navRadios[0].signal = data;
                this.publisher.publishSignalStrength(1, data);
            });
            this.simVarSubscriber.on('nav_signal_2').withPrecision(0).handle((data) => {
                this.navRadios[1].signal = data;
                this.publisher.publishSignalStrength(2, data);
            });
            this.simVarSubscriber.on('adf_active_frequency_1').whenChanged().handle((freq) => {
                this.publisher.publishAdfActiveFrequencySet(freq);
            });
            this.simVarSubscriber.on('adf_standby_frequency_1').whenChanged().handle((freq) => {
                this.publisher.publishAdfStandbyFrequencySet(freq);
            });
            // Configure and publish the initial state of all our radios.
            this.navRadios[0].selected = true;
            this.comRadios[0].selected = true;
            for (let i = 0; i < this.navRadios.length; i++) {
                this.updateAndPublish(this.navRadios[i]);
            }
            for (let i = 0; i < this.comRadios.length; i++) {
                this.updateAndPublish(this.comRadios[i]);
            }
        }
        /**
         * Perform events for the update loop.
         */
        onUpdate() {
            // noop
        }
        /**
         * Get the current frequency of a radio.
         * @param radioType The RadioType to query.
         * @param index The index number of the desired radio.
         * @param bank The FrequencyBank to query.
         * @returns The frequency in MHz.
         */
        getFrequency(radioType, index, bank) {
            return SimVar.GetSimVarValue(`${radioType == exports.RadioType.Com ? 'COM' : 'NAV'} ${bank == exports.FrequencyBank.Active ? 'ACTIVE' : 'STANDBY'} FREQUENCY:${index}`, 'MHz');
        }
        /**
         * React to a change on a radio frequency simvar.
         * @param type The RadioType to update.
         * @param index Index of the radio in the internal array.
         * @param bank The FrequencyBank in the selected radio to update.
         * @param freq The new frequency in MHz.
         */
        updateRadioFreqCb(type, index, bank, freq) {
            // Note: 'index' here is the index of the radio in our internal array,
            // not the device index.  This is confusing, and we should probably use
            // different words for each of the two data points.
            // TODO Disambigurate radio device number"index" from index in internal array.
            const radioArr = type == exports.RadioType.Nav ? this.navRadios : this.comRadios;
            switch (bank) {
                case exports.FrequencyBank.Active:
                    radioArr[index].activeFrequency = freq;
                    this.publisher.publishFreqChange(radioArr[index], exports.FrequencyBank.Active, freq);
                    break;
                case exports.FrequencyBank.Standby:
                    radioArr[index].standbyFrequency = freq;
                    this.publisher.publishFreqChange(radioArr[index], exports.FrequencyBank.Standby, freq);
                    break;
            }
        }
        /**
         * Get the current selected radio in a collection of radios.
         * @param radios An array of Radios.
         * @returns The selected Radio in the array.
         */
        getSelectedRadio(radios) {
            for (const radio of radios) {
                if (radio.selected) {
                    return radio;
                }
            }
            return undefined;
        }
        /**
         * Swap frequencies in a radio.
         * @param radio The radio whose frequencies we want to swap.
         */
        swapFreqs(radio) {
            if (radio !== undefined) {
                this.setKVar('SWAP', radio);
            }
        }
        /**
         * Update the frequencies in a radio from simvars.
         *
         * This is useful for snapshot updates as long as we're not worried
         * about one of the frequencies being updated while the snapshot is in
         * flight.
         * @param radio the radio to update
         */
        updateAndPublish(radio) {
            if (radio !== undefined) {
                radio.activeFrequency = this.getFrequency(radio.radioType, radio.index, exports.FrequencyBank.Active);
                radio.standbyFrequency = this.getFrequency(radio.radioType, radio.index, exports.FrequencyBank.Standby);
            }
            switch (radio === null || radio === void 0 ? void 0 : radio.radioType) {
                case exports.RadioType.Com:
                    this.comRadios[radio.index - 1] = radio;
                    break;
                case exports.RadioType.Nav:
                    this.navRadios[radio.index - 1] = radio;
                    break;
            }
            this.publisher.publishRadioState(radio);
        }
        /**
         * Explicitly set a new selected nav radio.
         * @param navSourceId An array of Radios to toggle.
         */
        setActiveRadio(navSourceId) {
            if (navSourceId.type === exports.NavSourceType.Nav) {
                for (let i = 0; i < this.navRadios.length; i++) {
                    const radio = this.navRadios[i];
                    if (radio.index == navSourceId.index) {
                        radio.selected = true;
                    }
                    else {
                        radio.selected = false;
                    }
                    this.publisher.publishRadioState(radio);
                }
            }
        }
        /**
         * Increase the volume of the selected nav or com radio.
         * @param radio The radio whose volume we want to increase.
         */
        volumeInc(radio) {
            if (radio !== undefined) {
                SimVar.SetSimVarValue(`K:${radio.radioType}${radio.index}_VOLUME_INC`, 'number', 0);
            }
        }
        /**
         * Increase the volume of the selected nav or com radio.
         * @param radio The radio whose volume we want to increase.
         */
        volumeDec(radio) {
            if (radio !== undefined) {
                SimVar.SetSimVarValue(`K:${radio.radioType}${radio.index}_VOLUME_DEC`, 'number', 0);
            }
        }
        /**
         * Increase the integer portion of a frequency.
         * @param radio The Radio to update.
         */
        wholeInc(radio) {
            this.setKVar('WHOLE_INC', radio);
        }
        /**
         * Decrease the integer portion of a frequency.
         * @param radio The Radio to update.
         */
        wholeDec(radio) {
            this.setKVar('WHOLE_DEC', radio);
        }
        /**
         * Increase the decimal portion of a frequency.
         * @param radio The Radio to update.
         */
        fractInc(radio) {
            this.setKVar('FRACT_INC', radio);
        }
        /**
         * Decrease the decimal portion of a frequency.
         * @param radio The Radio to update.
         */
        fractDec(radio) {
            this.setKVar('FRACT_DEC', radio);
        }
        /**
         * Set the standby frequency of the currently selected nav or com radio.
         * @param radioType The radio type we want to set standby for.
         * @param frequency The frequency in MHz as a string.
         */
        setStandbyFreq(radioType, frequency) {
            let radio;
            switch (radioType) {
                case exports.RadioType.Com:
                    radio = this.getSelectedRadio(this.comRadios);
                    break;
                case exports.RadioType.Nav:
                    radio = this.getSelectedRadio(this.navRadios);
                    break;
            }
            this.freqSet(radio, exports.FrequencyBank.Standby, frequency);
        }
        /**
         * Toggle which of the radios is selected.
         * @param radios An array of Radios to toggle.
         */
        swapSelection(radios) {
            // TODO It would be nice to extend this to handle systems with more than 2 radios
            for (let i = 0; i < radios.length; i++) {
                radios[i].selected = !radios[i].selected;
                this.publisher.publishRadioState(radios[i]);
            }
        }
        /**
         * Set the full frequency of a radio.
         * @param radio The Radio to update.
         * @param bank The FrequencyBank to update.
         * @param freq The new frequency in MHz as a string.
         */
        freqSet(radio, bank, freq) {
            if (!radio) {
                return;
            }
            let radioId;
            if (radio.radioType == exports.RadioType.Com) {
                const first = radio.index == 1 ? 'COM' : `COM${radio.index}`;
                const second = bank == exports.FrequencyBank.Active ? 'RADIO' : 'STBY_RADIO';
                radioId = `${first}_${second}`;
            }
            else {
                radioId = `NAV${radio.index}_${bank == exports.FrequencyBank.Active ? 'RADIO' : 'STBY'}`;
            }
            const freqMhz = Math.round(parseFloat(freq) * 1000) / 1000;
            SimVar.SetSimVarValue(`K:${radioId}_SET_HZ`, 'Hz', (Math.round(freqMhz * 1000000)));
        }
        /**
         * Set the K var for a frequency event
         * @param action A string defining whole/fract and inc/dec.
         * @param radio The radio this frequency is for.
         */
        setKVar(action, radio) {
            if (radio == undefined) {
                return;
            }
            let device;
            switch (radio.radioType) {
                case exports.RadioType.Nav:
                    device = `NAV${radio.index}`;
                    break;
                case exports.RadioType.Com:
                    if (action == 'SWAP') {
                        // Com radios break the naming pattern for swap events. :(
                        device = radio.index == 1 ? 'COM_STBY' : `COM${radio.index}`;
                    }
                    else {
                        device = radio.index == 1 ? 'COM' : `COM${radio.index}`;
                    }
                    break;
                default: // this should never happen
                    return;
            }
            SimVar.SetSimVarValue(`K:${device}_RADIO_${action}`, 'number', 0);
        }
        /**
         * Send an update of all our radio states.
         * @param data True if we really want to do this.  (We need to support non-paramaterized commands.())
         */
        publishRadioStates(data) {
            if (!data) {
                return;
            }
            for (const radio of this.navRadios) {
                this.publisher.publishRadioState(radio);
            }
            for (const radio of this.comRadios) {
                this.publisher.publishRadioState(radio);
            }
        }
        /**
         * Sets the COM frequency spacing.
         * @param evt The event that is setting the spacing.
         */
        setComSpacing(evt) {
            const currentSpacing = SimVar.GetSimVarValue(`COM SPACING MODE:${evt.index}`, exports.SimVarValueType.Enum);
            if (currentSpacing !== evt.spacing) {
                SimVar.SetSimVarValue(`K:COM_${evt.index.toFixed(0)}_SPACING_MODE_SWITCH`, 'number', 0);
            }
        }
    }

    /**
     * A publisher for pitot tube information.
     */
    class PitotPublisher extends SimVarPublisher {
        /**
         * Creates an instance of an PitotPublisher.
         * @param bus The event bus to use with this instance.
         * @param pitotCount The number of pitot tubes to support.
         * @param pacer An optional pacer to use to control the rate of publishing.
         */
        constructor(bus, pitotCount, pacer) {
            const indexedSimVars = [
                ['pitot_heat_switch_on', { name: 'PITOT HEAT SWITCH', type: exports.SimVarValueType.Bool }]
            ];
            const simvars = new Map(PitotPublisher.nonIndexedSimVars);
            // add pitot-indexed simvars
            for (const [topic, simvar] of indexedSimVars) {
                for (let i = 1; i <= pitotCount; i++) {
                    simvars.set(`${topic}_${i}`, {
                        name: `${simvar.name}:${i}`,
                        type: simvar.type,
                        map: simvar.map
                    });
                }
            }
            super(simvars, bus, pacer);
        }
    }
    PitotPublisher.nonIndexedSimVars = [
        ['pitot_heat_on', { name: 'PITOT HEAT', type: exports.SimVarValueType.Bool }],
        ['pitot_icing_pct', { name: 'PITOT ICE PCT', type: exports.SimVarValueType.Percent }]
    ];

    /**
     * A publisher for Pressurization information.
     */
    class PressurizationPublisher extends SimVarPublisher {
        /**
         * Updates the ADC publisher.
         */
        onUpdate() {
            super.onUpdate();
        }
        /**
         * Create an PressurizationPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer = undefined) {
            super(PressurizationPublisher.simvars, bus, pacer);
        }
    }
    PressurizationPublisher.simvars = new Map([
        ['cabin_altitude', { name: 'PRESSURIZATION CABIN ALTITUDE', type: exports.SimVarValueType.Feet }],
        ['cabin_altitude_rate', { name: 'PRESSURIZATION CABIN ALTITUDE RATE', type: exports.SimVarValueType.FPM }],
        ['pressure_diff', { name: 'PRESSURIZATION PRESSURE DIFFERENTIAL', type: exports.SimVarValueType.PSI }]
    ]);

    /**
     * A simple timer for handling debounce.
     */
    class DebounceTimer {
        constructor() {
            this.timer = null;
        }
        /**
         * Checks whether an action is pending on this timer.
         * @returns Whether an action is pending on this timer.
         */
        isPending() {
            return this.timer !== null;
        }
        /**
         * Schedules an action. Waits for a specified amount of time, and executes the action only if no other action is
         * scheduled on this timer during the delay.
         * @param action The action to schedule.
         * @param delay The debounce delay, in milliseconds.
         */
        schedule(action, delay) {
            this.clear();
            this.timer = setTimeout(() => {
                this.timer = null;
                action();
            }, delay);
        }
        /**
         * Clears this timer of any pending actions. Actions that are cleared will not be executed.
         */
        clear() {
            if (this.timer === null) {
                return;
            }
            clearTimeout(this.timer);
            this.timer = null;
        }
    }

    /** Transponder modes. */
    exports.XPDRMode = void 0;
    (function (XPDRMode) {
        XPDRMode[XPDRMode["OFF"] = 0] = "OFF";
        XPDRMode[XPDRMode["STBY"] = 1] = "STBY";
        XPDRMode[XPDRMode["TEST"] = 2] = "TEST";
        XPDRMode[XPDRMode["ON"] = 3] = "ON";
        XPDRMode[XPDRMode["ALT"] = 4] = "ALT";
        XPDRMode[XPDRMode["GROUND"] = 5] = "GROUND";
    })(exports.XPDRMode || (exports.XPDRMode = {}));
    /** A publiher to poll transponder simvars. */
    class XPDRSimVarPublisher extends SimVarPublisher {
        /**
         * Create an XPDRSimVarPublisher.
         * @param bus The EventBus to publish to.
         * @param pacer An optional pacer to use to control the pace of publishing.
         * @param transponderCount The number of transponders supported by this publisher.
         */
        constructor(bus, pacer = undefined, transponderCount = 1) {
            const vars = [];
            for (let i = 0; i < transponderCount; i++) {
                vars.push([`xpdr_mode_${i + 1}`, { name: `TRANSPONDER STATE:${i + 1}`, type: exports.SimVarValueType.Number }]);
                vars.push([`xpdr_code_${i + 1}`, { name: `TRANSPONDER CODE:${i + 1}`, type: exports.SimVarValueType.Number }]);
                vars.push([`xpdr_ident_${i + 1}`, { name: `TRANSPONDER IDENT:${i + 1}`, type: exports.SimVarValueType.Bool }]);
            }
            super(new Map(vars), bus, pacer);
        }
    }
    /** A transponder. */
    class XPDRInstrument {
        /**
         * Create an XPDRInstrument.
         * @param bus The event bus to publish to.
         * @param transponderCount The number of transponders supported by this instrument. Defaults to `1`.
         */
        constructor(bus, transponderCount = 1) {
            this.bus = bus;
            this.transponderCount = transponderCount;
            this.identDebounceTimers = Array.from({ length: this.transponderCount }, () => new DebounceTimer());
            this.bus = bus;
            this.simVarPublisher = new XPDRSimVarPublisher(bus);
            this.controlSubscriber = bus.getSubscriber();
        }
        /** Initialize the instrument. */
        init() {
            this.simVarPublisher.startPublish();
            for (let i = 0; i < this.transponderCount; i++) {
                this.controlSubscriber.on(`publish_xpdr_code_${i + 1}`).handle(this.setXpdrCode.bind(this, i + 1));
                this.controlSubscriber.on(`publish_xpdr_mode_${i + 1}`).handle(this.setXpdrMode.bind(this, i + 1));
                this.controlSubscriber.on(`xpdr_send_ident_${i + 1}`).handle(this.sendIdent.bind(this, i + 1));
                // force standby on plane load when off
                if (this.getXpdrMode(i + 1) === exports.XPDRMode.OFF) {
                    this.setXpdrMode(i + 1, exports.XPDRMode.STBY);
                }
            }
        }
        /**
         * Perform events for the update loop.
         */
        onUpdate() {
            // Currently, we just need to update our simvar publisher so it polls.
            this.simVarPublisher.onUpdate();
        }
        /**
         * Set the transponder code in the sim.
         * @param index The index of the transponder.
         * @param code The xpdr code.
         */
        setXpdrCode(index, code) {
            const bcdCode = Avionics.Utils.make_xpndr_bcd16(code);
            SimVar.SetSimVarValue(`K:${index}:XPNDR_SET`, 'Frequency BCD16', bcdCode);
        }
        /**
         * Set the transponder mode in the sim.
         * @param index The index of the transponder.
         * @param mode The transponder mode.
         */
        setXpdrMode(index, mode) {
            SimVar.SetSimVarValue(`TRANSPONDER STATE:${index}`, 'number', mode);
        }
        /**
         * Gets xpdr mode from the sim.
         * @param index The index of the transponder.
         * @returns The xpdr mode.
         */
        getXpdrMode(index) {
            return SimVar.GetSimVarValue(`TRANSPONDER STATE:${index}`, 'number');
        }
        /**
         * Sends ident to ATC for 18 seconds.
         * @param index The index of the transponder.
         */
        sendIdent(index) {
            if (this.getXpdrMode(index) > exports.XPDRMode.STBY) {
                SimVar.SetSimVarValue(`K:${index}:XPNDR_IDENT_ON`, 'number', 1);
                this.identDebounceTimers[index - 1].schedule(() => {
                    SimVar.SetSimVarValue(`K:${index}:XPNDR_IDENT_OFF`, 'number', 0);
                }, 18000);
            }
        }
    }

    /**
     * A utility class for generating Promises that wait for certain conditions before they are fulfilled.
     */
    class Wait {
        /**
         * Waits for a set amount of time.
         * @param delay The amount of time to wait in milliseconds.
         * @returns a Promise which is fulfilled after the delay.
         */
        static awaitDelay(delay) {
            return new Promise(resolve => setTimeout(() => resolve(), delay));
        }
        /**
         * Waits for a certain number of frames to elapse.
         * @param count The number of frames to wait.
         * @param glassCockpitRefresh Whether to wait for glass cockpit refresh frames instead of CoherentGT frames. Defaults
         * to `false`.
         */
        static awaitFrames(count, glassCockpitRefresh = false) {
            let elapsedFrameCount = 0;
            if (glassCockpitRefresh) {
                return new Promise(resolve => {
                    const callback = () => {
                        if (++elapsedFrameCount > count) {
                            resolve();
                        }
                        else {
                            requestAnimationFrame(callback);
                        }
                    };
                    requestAnimationFrame(callback);
                });
            }
            else {
                return new Promise(resolve => {
                    const id = setInterval(() => {
                        if (++elapsedFrameCount > count) {
                            clearInterval(id);
                            resolve();
                        }
                    }, 0);
                });
            }
        }
        /**
         * Waits for a condition to be satisfied.
         * @param predicate A function which evaluates whether the condition is satisfied.
         * @param interval The interval, in milliseconds, at which to evaluate the condition. A zero or negative value
         * causes the condition to be evaluated every frame. Defaults to 0.
         * @param timeout The amount of time, in milliseconds, before the returned Promise is rejected if the condition is
         * not satisfied. A zero or negative value causes the Promise to never be rejected and the condition to be
         * continually evaluated until it is satisfied. Defaults to 0.
         * @returns a Promise which is fulfilled when the condition is satisfied.
         */
        static awaitCondition(predicate, interval = 0, timeout = 0) {
            const t0 = Date.now();
            if (interval <= 0) {
                const loopFunc = (resolve, reject) => {
                    if (timeout > 0 && Date.now() - t0 >= timeout) {
                        reject('Await condition timed out.');
                    }
                    else {
                        predicate() ? resolve() : requestAnimationFrame(loopFunc.bind(undefined, resolve, reject));
                    }
                };
                return new Promise((resolve, reject) => { loopFunc(resolve, reject); });
            }
            else {
                return new Promise((resolve, reject) => {
                    const timer = setInterval(() => {
                        if (timeout > 0 && Date.now() - t0 > timeout) {
                            clearInterval(timer);
                            reject('Await condition timed out.');
                        }
                        else if (predicate()) {
                            clearInterval(timer);
                            resolve();
                        }
                    }, interval);
                });
            }
        }
        /**
         * Waits for a notification from a {@link Subscribable}, with an optional condition to end the wait based on the value
         * of the subscribable.
         * @param subscribable The subscribable to wait for.
         * @param predicate A function which evaluates whether the value of the subscribable satisfies the condition for the
         * wait to end. If not defined, any value is considered satisfactory.
         * @param initialCheck Whether to immediately receive a notification from the subscribable at the start of the wait.
         * Defaults to `false`.
         * @param timeout The amount of time, in milliseconds, before the returned Promise is rejected if the condition is
         * not satisfied. A zero or negative value causes the Promise to never be rejected. Defaults to 0.
         * @returns A Promise which is fulfilled with the value of the subscribable when a notification is received with a
         * value that satisfies the condition for the wait to end.
         */
        static awaitSubscribable(subscribable, predicate, initialCheck = false, timeout = 0) {
            return new Promise((resolve, reject) => {
                const sub = subscribable.sub(val => {
                    if (predicate === undefined || predicate(val)) {
                        sub.destroy();
                        resolve(val);
                    }
                }, false, true);
                sub.resume(initialCheck);
                if (timeout > 0) {
                    setTimeout(() => {
                        if (sub.isAlive) {
                            sub.destroy();
                            reject('Await condition timed out.');
                        }
                    }, timeout);
                }
            });
        }
        /**
         * Waits for an event from a {@link Consumer}, with an optional condition to end the wait based on the value of the
         * consumed event.
         * @param consumer The event consumer to wait for.
         * @param predicate A function which evaluates whether the value of the consumed event satisfies the condition for
         * the wait to end. If not defined, any value is considered satisfactory.
         * @param initialCheck Whether to immediately receive an event from the event consumer at the start of the wait.
         * Defaults to `false`.
         * @param timeout The amount of time, in milliseconds, before the returned Promise is rejected if the condition is
         * not satisfied. A zero or negative value causes the Promise to never be rejected. Defaults to 0.
         * @returns A Promise which is fulfilled with the value of the consumed event when an event is received with a
         * value that satisfies the condition for the wait to end.
         */
        static awaitConsumer(consumer, predicate, initialCheck = false, timeout = 0) {
            return new Promise((resolve, reject) => {
                const sub = consumer.handle(val => {
                    if (predicate === undefined || predicate(val)) {
                        sub.destroy();
                        resolve(val);
                    }
                }, true);
                sub.resume(initialCheck);
                if (timeout > 0) {
                    setTimeout(() => {
                        if (sub.isAlive) {
                            sub.destroy();
                            reject('Await condition timed out.');
                        }
                    }, timeout);
                }
            });
        }
        /**
         * Waits for an event from a {@link ReadonlySubEvent}, with an optional condition to end the wait based on the sender
         * and data of the event.
         * @param event The event to wait for.
         * @param predicate A function which evaluates whether the sender and data of the event satisfy the condition for
         * the wait to end. If not defined, any sender/data is considered satisfactory.
         * @param timeout The amount of time, in milliseconds, before the returned Promise is rejected if the condition is
         * not satisfied. A zero or negative value causes the Promise to never be rejected. Defaults to 0.
         * @returns A Promise which is fulfilled with the data of the event when an event is received with a sender and data
         * that satisfy the condition for the wait to end.
         */
        static awaitSubEvent(event, predicate, timeout = 0) {
            return new Promise((resolve, reject) => {
                const sub = event.on((sender, data) => {
                    if (predicate === undefined || predicate(data, sender)) {
                        sub.destroy();
                        resolve(data);
                    }
                }, true);
                sub.resume();
                if (timeout > 0) {
                    setTimeout(() => {
                        if (sub.isAlive) {
                            sub.destroy();
                            reject('Await condition timed out.');
                        }
                    }, timeout);
                }
            });
        }
    }

    /**
     * Tracks aircraft traffic. Maintains a list of contacts, periodically updates their position, altitude, and reported
     * heading, and uses these data to compute ground speed, ground track, and vertical speed.
     */
    class TrafficInstrument {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param options Options with which to initialize this instrument.
         */
        constructor(bus, options) {
            this.bus = bus;
            this.tracked = new Map();
            this.lastUpdateRealTime = 0;
            this.lastUpdateSimTime = 0;
            this.isBusy = false;
            this.options = Object.assign({}, options);
        }
        /**
         * Retrieves a traffic contact by its assigned ID number.
         * @param uid an ID number.
         * @returns the traffic contact with the assigned ID number, or undefined if no such contact exists.
         */
        getContact(uid) {
            return this.tracked.get(uid);
        }
        /**
         * Iterates through all tracked traffic contacts with a visitor function.
         * @param visitor A visitor function.
         */
        forEachContact(visitor) {
            this.tracked.forEach(visitor);
        }
        /**
         * Initializes this instrument. Once initialized, this instrument will automatically track and update traffic
         * contacts.
         */
        init() {
            this.bus.getSubscriber()
                .on('simTime')
                .whenChanged()
                .handle(this.onSimTimeChanged.bind(this));
        }
        /**
         * Updates this instrument's list of contacts.
         * @param data An array of the most recent traffic data entries.
         * @param simTime The sim time at which the traffic data was generated.
         */
        updateContacts(data, simTime) {
            const len = data.length;
            for (let i = 0; i < len; i++) {
                const entry = data[i];
                const contact = this.tracked.get(entry.uId);
                if (contact) {
                    this.updateContact(contact, entry, simTime);
                }
                else {
                    this.createContact(entry, simTime);
                }
            }
        }
        /**
         * Creates a contact.
         * @param entry The traffic data entry from which to create the new contact.
         * @param simTime The sim time at which the traffic data entry was generated.
         */
        createContact(entry, simTime) {
            const contact = new TrafficContactClass(entry.uId, 1000 / this.options.simTimeUpdateFreq * 5);
            this.tracked.set(contact.uid, contact);
            contact.update(entry.lat, entry.lon, UnitType.METER.convertTo(entry.alt, UnitType.FOOT), entry.heading, simTime);
            this.bus.pub('traffic_contact_added', contact.uid, false, false);
        }
        /**
         * Updates a contact.
         * @param contact The contact to update.
         * @param entry The current traffic data entry for the contact.
         * @param simTime The sim time at which the traffic data entry was generated.
         */
        updateContact(contact, entry, simTime) {
            contact.update(entry.lat, entry.lon, UnitType.METER.convertTo(entry.alt, UnitType.FOOT), entry.heading, simTime);
            this.bus.pub('traffic_contact_updated', contact.uid, false, false);
        }
        /**
         * Removes all contacts whose time since last contact exceeds the deprecation threshold.
         * @param simTime The current sim time.
         */
        deprecateContacts(simTime) {
            this.tracked.forEach(contact => {
                const dt = Math.abs(simTime - contact.lastContactTime);
                if (dt >= this.options.contactDeprecateTime) {
                    this.tracked.delete(contact.uid);
                    this.bus.pub('traffic_contact_removed', contact.uid, false, false);
                }
            });
        }
        /**
         * A callback which is called when the sim time changes.
         * @param simTime The current sim time.
         */
        async onSimTimeChanged(simTime) {
            const realTime = Date.now();
            if (this.isBusy
                || Math.abs(simTime - this.lastUpdateSimTime) < 1000 / this.options.simTimeUpdateFreq
                || Math.abs(realTime - this.lastUpdateRealTime) < 1000 / this.options.realTimeUpdateFreq) {
                return;
            }
            this.isBusy = true;
            try {
                const data = await Promise.race([Coherent.call('GET_AIR_TRAFFIC'), Wait.awaitDelay(1000)]);
                if (data) {
                    this.updateContacts(data, simTime);
                    this.deprecateContacts(simTime);
                    this.lastUpdateSimTime = simTime;
                    this.lastUpdateRealTime = realTime;
                }
            }
            catch (e) {
                console.error(e);
                if (e instanceof Error) {
                    console.error(e.stack);
                }
            }
            this.isBusy = false;
        }
        /**
         * This method does nothing.
         */
        onUpdate() {
            // noop
        }
    }
    /**
     * An aircraft contact that is being tracked. Each contact tracks its last reported position, altitude, and heading.
     * Successively updating these values will allow ground speed, ground track, and vertical speed to be calculated based
     * on changes in the values over time. The calculated values are exponentially smoothed to reduce artifacts from
     * potentially noisy data.
     */
    class TrafficContactClass {
        // eslint-disable-next-line jsdoc/require-jsdoc
        get lastHeading() {
            return this._lastHeading;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        get lastContactTime() {
            return this._lastContactTime;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        get groundTrack() {
            return this._groundTrack;
        }
        /**
         * Constructor.
         * @param uid This contact's unique ID number.
         * @param contactTimeResetThreshold The maximum allowed elapsed sim time, in milliseconds, since time of last contact
         * before this contact's computed values are reset.
         */
        constructor(uid, contactTimeResetThreshold) {
            this.uid = uid;
            this.contactTimeResetThreshold = contactTimeResetThreshold;
            // reported data
            this._lastPosition = new GeoPoint(NaN, NaN);
            this.lastPosition = this._lastPosition.readonly;
            this._lastAltitude = UnitType.FOOT.createNumber(NaN);
            this.lastAltitude = this._lastAltitude.readonly;
            this._lastHeading = NaN;
            this._lastContactTime = NaN;
            // computed data
            this._groundSpeed = UnitType.KNOT.createNumber(NaN);
            this.groundSpeed = this._groundSpeed.readonly;
            this._groundTrack = NaN;
            this._verticalSpeed = UnitType.FPM.createNumber(NaN);
            this.verticalSpeed = this._verticalSpeed.readonly;
            this.groundSpeedSmoother = new ExpSmoother(TrafficContactClass.GROUND_SPEED_TIME_CONSTANT, null, this.contactTimeResetThreshold / 1000);
            this.groundTrackSmoother = new ExpSmoother(TrafficContactClass.GROUND_TRACK_TIME_CONSTANT, null, this.contactTimeResetThreshold / 1000);
            this.verticalSpeedSmoother = new ExpSmoother(TrafficContactClass.VERTICAL_SPEED_TIME_CONSTANT, null, this.contactTimeResetThreshold / 1000);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        predict(simTime, positionOut, altitudeOut) {
            if (this.groundSpeed.isNaN()) {
                positionOut.set(NaN, NaN);
                altitudeOut.set(NaN);
                return;
            }
            const dt = simTime - this.lastContactTime;
            const distance = UnitType.NMILE.convertTo(this._groundSpeed.number * (dt / 3600000), UnitType.GA_RADIAN);
            this._lastPosition.offset(this._groundTrack, distance, positionOut);
            const deltaAlt = this._verticalSpeed.number * (dt / 60000);
            this._lastAltitude.add(deltaAlt, UnitType.FOOT, altitudeOut);
        }
        /**
         * Updates this contact with the current reported position, altitude and heading. Also updates the computed ground
         * speed, ground track, and vertical speed if there are sufficient data to do so.
         * @param lat The current reported latitude.
         * @param lon The current reported longitude.
         * @param altitude The current reported altitude, in feet.
         * @param heading The current reported heading.
         * @param simTime The current sim time.
         */
        update(lat, lon, altitude, heading, simTime) {
            const dt = simTime - this._lastContactTime;
            if (!isNaN(dt) && (dt < 0 || dt > this.contactTimeResetThreshold)) {
                this.reset(lat, lon, altitude, heading, simTime);
                return;
            }
            if (!isNaN(dt) && dt > 0) {
                this.updateComputedValues(dt / 1000, lat, lon, altitude);
            }
            this.setReportedValues(lat, lon, altitude, heading);
            if (this.areComputedValuesValid()) {
                this._lastContactTime = simTime;
            }
            else {
                this.reset(lat, lon, altitude, heading, simTime);
            }
        }
        /**
         * Erases this contact's tracking history and sets the initial reported position, altitude, and heading.
         * @param lat The current reported latitude.
         * @param lon The current reported longitude.
         * @param altitude The current reported altitude, in feet.
         * @param heading The current reported heading.
         * @param simTime The current sim time.
         */
        reset(lat, lon, altitude, heading, simTime) {
            this.setReportedValues(lat, lon, altitude, heading);
            this._groundSpeed.set(NaN);
            this._groundTrack = NaN;
            this._verticalSpeed.set(NaN);
            this.groundSpeedSmoother.reset();
            this.groundTrackSmoother.reset();
            this.verticalSpeedSmoother.reset();
            this._lastContactTime = simTime;
        }
        /**
         * Sets the most recent reported values.
         * @param lat The reported latitude.
         * @param lon The reported longitude.
         * @param altitude The reported altitude, in feet.
         * @param heading The reported heading.
         */
        setReportedValues(lat, lon, altitude, heading) {
            this._lastPosition.set(lat, lon);
            this._lastAltitude.set(altitude);
            this._lastHeading = heading;
        }
        /**
         * Updates this contact's computed values.
         * @param dt The elapsed time, in seconds, since last contact.
         * @param lat The current reported latitude.
         * @param lon The current reported longitude.
         * @param altitude The current reported altitude, in feet.
         */
        updateComputedValues(dt, lat, lon, altitude) {
            const pos = TrafficContactClass.tempGeoPoint.set(lat, lon);
            const distanceNM = UnitType.GA_RADIAN.convertTo(this.lastPosition.distance(pos), UnitType.NMILE);
            const track = pos.bearingFrom(this._lastPosition);
            this.updateGroundSpeed(dt, distanceNM);
            this.updateGroundTrack(dt, track, distanceNM);
            this.updateVerticalSpeed(dt, altitude);
        }
        /**
         * Updates this contact's ground speed.
         * @param dt The elapsed time, in seconds, since last contact.
         * @param distanceNM The distance, in nautical miles, from this contact's position at last contact to this contact's
         * current reported position.
         */
        updateGroundSpeed(dt, distanceNM) {
            const dtHours = dt / 3600;
            const speedKnots = distanceNM / dtHours;
            this._groundSpeed.set(this.groundSpeedSmoother.next(speedKnots, dt));
        }
        /**
         * Updates this contact's ground track.
         * @param dt The elapsed time, in seconds, since last contact.
         * @param track The true ground track from this contact's position at last contact to this contact's current reported
         * position, as measured at the current reported position.
         * @param distanceNM The distance, in nautical miles, from this contact's position at last contact to this contact's
         * current reported position.
         */
        updateGroundTrack(dt, track, distanceNM) {
            const last = this.groundTrackSmoother.last();
            if (distanceNM >= TrafficContactClass.MIN_GROUND_TRACK_DISTANCE) {
                if (last !== null && !isNaN(last)) {
                    // need to handle wraparounds
                    let delta = track - last;
                    if (delta > 180) {
                        delta = delta - 360;
                    }
                    else if (delta < -180) {
                        delta = delta + 360;
                    }
                    track = last + delta;
                }
            }
            else {
                // if distance between current and last position is too small, computed ground track will be unreliable
                // (and if distance = 0 the track will be meaningless), so we just copy forward the last computed track,
                // or NaN if there is no previously computed track
                track = last === null ? NaN : last;
            }
            const next = last !== null && isNaN(last) ? this.groundTrackSmoother.reset(track) : this.groundTrackSmoother.next(track, dt);
            this._groundTrack = (next + 360) % 360; // enforce range 0-359
        }
        /**
         * Updates this contact's vertical speed.
         * @param dt The elapsed time, in seconds, since last contact.
         * @param altitude The current reported altitude, in feet.
         */
        updateVerticalSpeed(dt, altitude) {
            const dtMin = dt / 60;
            const deltaAltFeet = altitude - this._lastAltitude.number;
            const vsFPM = deltaAltFeet / dtMin;
            this._verticalSpeed.set(this.verticalSpeedSmoother.next(vsFPM, dt));
        }
        /**
         * Checks whether this contact's calculated ground speed and vertical speeds are valid.
         * @returns whether this contact's calculated ground speed and vertical speeds are valid.
         */
        areComputedValuesValid() {
            const isGroundSpeedValid = this._groundSpeed.isNaN() || this._groundSpeed.number <= TrafficContactClass.MAX_VALID_GROUND_SPEED;
            const isVerticalSpeedValid = this._verticalSpeed.isNaN() || this._verticalSpeed.number <= TrafficContactClass.MAX_VALID_VERTICAL_SPEED;
            return isGroundSpeedValid && isVerticalSpeedValid;
        }
    }
    TrafficContactClass.GROUND_SPEED_TIME_CONSTANT = 2 / Math.LN2;
    TrafficContactClass.GROUND_TRACK_TIME_CONSTANT = 2 / Math.LN2;
    TrafficContactClass.VERTICAL_SPEED_TIME_CONSTANT = 2 / Math.LN2;
    TrafficContactClass.MAX_VALID_GROUND_SPEED = 1500; // knots
    TrafficContactClass.MAX_VALID_VERTICAL_SPEED = 10000; // fpm
    TrafficContactClass.MIN_GROUND_TRACK_DISTANCE = 10 / 1852; // nautical miles
    TrafficContactClass.tempGeoPoint = new GeoPoint(0, 0);

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A publisher for fuel system information.
     */
    class FuelSystemSimVarPublisher extends SimVarPublisher {
        /**
         * Create an FuelSystemSimvarPublisher
         * @param bus The EventBus to publish to
         * @param pacer An optional pacer to use to control the rate of publishing
         */
        constructor(bus, pacer = undefined) {
            const simvars = new Map([
                ['fuel_system_valve_switch', { name: 'FUELSYSTEM VALVE SWITCH:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['fuel_system_valve_open', { name: 'FUELSYSTEM VALVE OPEN:#index#', type: exports.SimVarValueType.Number, indexed: true }],
                ['fuel_system_pump_switch', { name: 'FUELSYSTEM PUMP SWITCH:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['fuel_system_pump_active', { name: 'FUELSYSTEM PUMP ACTIVE:#index#', type: exports.SimVarValueType.Bool, indexed: true }],
                ['fuel_system_engine_pressure', { name: 'FUELSYSTEM ENGINE PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true }],
                ['fuel_system_line_pressure', { name: 'FUELSYSTEM LINE FUEL PRESSURE:#index#', type: exports.SimVarValueType.PSI, indexed: true }],
                ['fuel_system_line_flow', { name: 'FUELSYSTEM LINE FUEL FLOW:#index#', type: exports.SimVarValueType.GPH, indexed: true }],
                ['fuel_system_tank_quantity', { name: 'FUELSYSTEM TANK QUANTITY:#index#', type: exports.SimVarValueType.GAL, indexed: true }],
            ]);
            super(simvars, bus, pacer);
        }
        /** @inheritdoc */
        onUpdate() {
            super.onUpdate();
        }
    }

    /**
     * A publisher for system alert messages.  This publisher works primarily with integers
     * that are indicies into an array of annunciation messages for the host instrument
     * as provided by the panel.xml configuration parser.
     * */
    class SystemAlertPublisher extends BasePublisher {
        /**
         * Publish an alert event.
         * @param event The event from SystemAlertEvents.
         * @param value The value for the event.
         */
        publishAlert(event, value) {
            this.publish(event, value, true, false);
        }
    }

    /** Utility functions for working with UUIDs. */
    class UUID {
        /**
         * A function to generate a spec-compliand v4 UUID in a 32-bit safe way.
         * @returns A UUID in standard 8-4-4-4-12 notation.
         */
        static GenerateUuid() {
            const scale = 2 ** 32;
            const first = UUID.bytesToHexString(Math.random() * scale);
            const fourth = UUID.bytesToHexString(Math.random() * scale);
            let secondBits = Math.random() * scale;
            let thirdBits = Math.random() * scale;
            // 4 MSB of seventh byte = 0100
            secondBits |= 0b00000000000000000100000000000000;
            secondBits &= 0b11111111111111110100111111111111;
            const second = UUID.bytesToHexString(secondBits);
            // 2 MSB of ninth byte = 10;
            thirdBits |= 0b10000000000000000000000000000000;
            thirdBits &= 0b10111111111111111111111111111111;
            const third = UUID.bytesToHexString(thirdBits);
            return `${first}-${second.substring(0, 4)}-${second.substring(4)}-${third.substring(0, 4)}-${third.substring(4)}${fourth}`;
        }
        /**
         * Take a number and return its hexadecimal representation.
         * @param bits The bytes to format.
         * @returns The input bits as a hexadecimal string.
         */
        static bytesToHexString(bits) {
            let string = (bits >>> 0).toString(16);
            string = '00000000'.substring(string.length) + string;
            return string;
        }
    }

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/utils/xmllogic" />
    /** The acceptable priority types for a given annunciation. */
    exports.AnnunciationType = void 0;
    (function (AnnunciationType) {
        AnnunciationType[AnnunciationType["Warning"] = 0] = "Warning";
        AnnunciationType[AnnunciationType["Caution"] = 1] = "Caution";
        AnnunciationType[AnnunciationType["Advisory"] = 2] = "Advisory";
        AnnunciationType[AnnunciationType["SafeOp"] = 3] = "SafeOp";
    })(exports.AnnunciationType || (exports.AnnunciationType = {}));
    /** The main logic for a cabin annunciation. */
    class Annunciation {
        /**
         * Creates an instance of Annunciation.
         * @param type The type of annuniciaton this is.
         * @param text The text label to show.
         * @param condition The logic condition for setting it.
         * @param suffix Any suffix text to past to the end.
         * @param uuid A preset UUID, or one will be generated.
         */
        constructor(type, text, condition, suffix, uuid) {
            this.type = type;
            this.text = text;
            this.condition = condition;
            this.suffix = suffix;
            this.uuid = uuid !== null && uuid !== void 0 ? uuid : UUID.GenerateUuid();
        }
    }

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/utils/xmllogic" />
    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/baseinstrument" />
    /** Create a list of annunciations from the instrument XML config. */
    class XMLAnnunciationFactory {
        /**
         * Create an XMLAnnunciationFactory.
         * @param instrument The instrument that holds this engine display.
         */
        constructor(instrument) {
            this.instrument = instrument;
        }
        /**
         * Parses a configuration document element and generates an array of annunciations defined by the element.
         * @param element The element to parse.
         * @returns An array of annunciations defined by the element.
         */
        parseConfigElement(element) {
            const annunciations = new Array();
            for (const ann of element.children) {
                let type;
                let suffix;
                const uuid = UUID.GenerateUuid();
                // Priority type that this alert has.
                const typeElem = ann.getElementsByTagName('Type');
                if (typeElem.length == 0) {
                    continue;
                }
                switch (typeElem[0].textContent) {
                    case 'Warning':
                        type = exports.AnnunciationType.Warning;
                        break;
                    case 'Caution':
                        type = exports.AnnunciationType.Caution;
                        break;
                    case 'Advisory':
                        type = exports.AnnunciationType.Advisory;
                        break;
                    case 'SafeOp':
                        type = exports.AnnunciationType.SafeOp;
                        break;
                    default:
                        continue;
                }
                // The actual text shown when the alert is displayed.
                const textElem = ann.getElementsByTagName('Text');
                if (textElem.length == 0 || textElem[0].textContent == null) {
                    continue;
                }
                const text = textElem[0].textContent;
                // Get the XML logic condition for state control.
                const condElem = ann.getElementsByTagName('Condition');
                if (condElem.length == 0) {
                    continue;
                }
                for (const condition of condElem) {
                    const logic = new CompositeLogicXMLElement(this.instrument, condition);
                    // A suffix put on the text when it's shown.
                    const suffElem = condition.getAttribute('Suffix');
                    if (suffElem !== null) {
                        suffix = suffElem;
                    }
                    else {
                        suffix = undefined;
                    }
                    annunciations.push(new Annunciation(type, text, logic, suffix, uuid));
                }
            }
            return annunciations;
        }
        /**
         * Parses a configuration document and generates an array of annunciations defined by the document. This method
         * looks for the first element in the document with a tag name equal to `Annunciations` and parses annunciations
         * from that element. If such an element does not exist, then an empty array will be returned.
         * @param document The configuration document to parse.
         * @returns An array of annunciations defined by the document.
         */
        parseConfig(document) {
            const configs = document.getElementsByTagName('Annunciations');
            if (configs.length === 0) {
                return [];
            }
            else {
                return this.parseConfigElement(configs[0]);
            }
        }
    }

    /**
     * Types of subscribable array change event.
     */
    exports.SubscribableArrayEventType = void 0;
    (function (SubscribableArrayEventType) {
        /** An element was added. */
        SubscribableArrayEventType["Added"] = "Added";
        /** An element was removed. */
        SubscribableArrayEventType["Removed"] = "Removed";
        /** The array was cleared. */
        SubscribableArrayEventType["Cleared"] = "Cleared";
    })(exports.SubscribableArrayEventType || (exports.SubscribableArrayEventType = {}));

    /**
     * An array-like class to observe changes in a list of objects.
     * @class ArraySubject
     * @template T
     */
    class AbstractSubscribableArray {
        constructor() {
            this.notifyDepth = 0;
            /** A function which sends initial notifications to subscriptions. */
            this.initialNotifyFunc = this.initialNotify.bind(this);
            /** A function which responds to when a subscription to this subscribable is destroyed. */
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /**
         * Adds a subscription to this array.
         * @param sub The subscription to add.
         */
        addSubscription(sub) {
            if (this.subs) {
                this.subs.push(sub);
            }
            else if (this.singletonSub) {
                this.subs = [this.singletonSub, sub];
                delete this.singletonSub;
            }
            else {
                this.singletonSub = sub;
            }
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Gets an item from the array.
         * @param index Thex index of the item to get.
         * @returns An item.
         * @throws
         */
        get(index) {
            const array = this.getArray();
            if (index > array.length - 1) {
                throw new Error('Index out of range');
            }
            return array[index];
        }
        /**
         * Tries to get the value from the array.
         * @param index The index of the item to get.
         * @returns The value or undefined if not found.
         */
        tryGet(index) {
            return this.getArray()[index];
        }
        /**
         * Notifies subscriptions of a change in the array.
         * @param index The index that was changed.
         * @param type The type of subject event.
         * @param modifiedItem The item modified by the operation.
         */
        notify(index, type, modifiedItem) {
            const canCleanUpSubs = this.notifyDepth === 0;
            let needCleanUpSubs = false;
            this.notifyDepth++;
            if (this.singletonSub) {
                try {
                    if (this.singletonSub.isAlive && !this.singletonSub.isPaused) {
                        this.singletonSub.handler(index, type, modifiedItem, this.getArray());
                    }
                }
                catch (error) {
                    console.error(`AbstractSubscribableArray: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
                if (canCleanUpSubs) {
                    // If subscriptions were added during the notification, then singletonSub would be deleted and replaced with
                    // the subs array.
                    if (this.singletonSub) {
                        needCleanUpSubs = !this.singletonSub.isAlive;
                    }
                    else if (this.subs) {
                        for (let i = 0; i < this.subs.length; i++) {
                            if (!this.subs[i].isAlive) {
                                needCleanUpSubs = true;
                                break;
                            }
                        }
                    }
                }
            }
            else if (this.subs) {
                const subLen = this.subs.length;
                for (let i = 0; i < subLen; i++) {
                    try {
                        const sub = this.subs[i];
                        if (!sub.isPaused) {
                            sub.handler(index, type, modifiedItem, this.getArray());
                        }
                        needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`AbstractSubscribableArray: error in handler: ${error}`);
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                // If subscriptions were added during the notification and a cleanup operation is not already pending, then we
                // need to check if any of the new subscriptions are already dead and if so, pend a cleanup operation.
                if (canCleanUpSubs && !needCleanUpSubs) {
                    for (let i = subLen; i < this.subs.length; i++) {
                        if (!this.subs[i].isAlive) {
                            needCleanUpSubs = true;
                            break;
                        }
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs) {
                if (this.singletonSub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    this.subs = this.subs.filter(sub => sub.isAlive);
                }
            }
        }
        /**
         * Notifies a subscription of this array's current state.
         * @param sub The subscription to notify.
         */
        initialNotify(sub) {
            const array = this.getArray();
            sub.handler(0, exports.SubscribableArrayEventType.Added, array, array);
        }
        /**
         * Responds to when a subscription to this array is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                if (this.singletonSub === sub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    const index = this.subs.indexOf(sub);
                    if (index >= 0) {
                        this.subs.splice(index, 1);
                    }
                }
            }
        }
    }

    /**
     * An array-like class to observe changes in a list of objects.
     * @class ArraySubject
     * @template T
     */
    class ArraySubject extends AbstractSubscribableArray {
        // eslint-disable-next-line jsdoc/require-returns
        /** The length of this array. */
        get length() {
            return this.array.length;
        }
        /**
         * Constructs an observable array.
         * @param arr The initial array elements.
         */
        constructor(arr) {
            super();
            this.array = arr;
        }
        /**
         * Creates and returns a new observable array.
         * @static
         * @template AT The type of the array items.
         * @param arr The initial array elements.
         * @returns A new instance of SubjectArray.
         */
        static create(arr = []) {
            return new ArraySubject(arr);
        }
        /**
         * Inserts a new item at the end or the specified index.
         * @param item The item to insert.
         * @param index The optional index to insert the item to. Will add the item at then end if index not given.
         */
        insert(item, index) {
            if (index === undefined || index > this.array.length - 1) {
                index = this.array.length;
                this.array.push(item);
            }
            else {
                this.array.splice(index, 0, item);
            }
            this.notify(index, exports.SubscribableArrayEventType.Added, item);
        }
        /**
         * Inserts items of an array beginning at the specified index.
         * @param [index] The index to begin inserting the array items.
         * @param arr The array to insert.
         */
        insertRange(index = 0, arr) {
            this.array.splice(index, 0, ...arr);
            this.notify(index, exports.SubscribableArrayEventType.Added, arr);
        }
        /**
         * Removes the item at the specified index.
         * @param index The index of the item to remove.
         */
        removeAt(index) {
            const removedItem = this.array.splice(index, 1);
            this.notify(index, exports.SubscribableArrayEventType.Removed, removedItem[0]);
        }
        /**
         * Removes the given item from the array.
         * @param item The item to remove.
         * @returns Returns a boolean indicating if the item was found and removed.
         */
        removeItem(item) {
            const index = this.array.indexOf(item);
            if (index > -1) {
                this.removeAt(index);
                return true;
            }
            else {
                return false;
            }
        }
        /**
         * Replaces all items in the array with the new array.
         * @param arr The array.
         */
        set(arr) {
            this.clear();
            this.insertRange(0, arr);
        }
        /**
         * Clears all data in the array.
         */
        clear() {
            this.array.length = 0;
            this.notify(0, exports.SubscribableArrayEventType.Cleared);
        }
        /**
         * Gets the array.
         * @returns The array.
         */
        getArray() {
            return this.array;
        }
    }

    /**
     * A object-valued subscribable subject which supports setting individual properties on the object and notifying
     * subscribers of any changes to those properties.
     */
    class ObjectSubject {
        /**
         * Constructs an observable object Subject.
         * @param obj The initial object.
         */
        constructor(obj) {
            this.obj = obj;
            this.isSubscribable = true;
            this.isMutableSubscribable = true;
            this.subs = [];
            this.notifyDepth = 0;
            this.initialNotifyFunc = this.initialNotify.bind(this);
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /**
         * Creates and returns a new ObjectSubject.
         * @param v The initial value of the subject.
         * @returns An ObjectSubject instance.
         */
        static create(v) {
            return new ObjectSubject(v);
        }
        /**
         * Gets this subject's object.
         * @returns This subject's object.
         */
        get() {
            return this.obj;
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Adds a subscription to this subscribable.
         * @param sub The subscription to add.
         */
        addSubscription(sub) {
            if (this.subs) {
                this.subs.push(sub);
            }
            else if (this.singletonSub) {
                this.subs = [this.singletonSub, sub];
                delete this.singletonSub;
            }
            else {
                this.singletonSub = sub;
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, value) {
            if (typeof arg1 === 'object') {
                for (const prop in arg1) {
                    if (prop in this.obj) {
                        this.set(prop, arg1[prop]);
                    }
                }
            }
            else {
                const oldValue = this.obj[arg1];
                if (value !== oldValue) {
                    this.obj[arg1] = value;
                    this.notify(arg1, oldValue);
                }
            }
        }
        /**
         * Notifies subscriptions that one of the properties of this subject's object has changed.
         * @param key The property of the object that changed.
         * @param oldValue The old value of the property that changed.
         */
        notify(key, oldValue) {
            const canCleanUpSubs = this.notifyDepth === 0;
            let needCleanUpSubs = false;
            this.notifyDepth++;
            if (this.singletonSub) {
                try {
                    if (this.singletonSub.isAlive && !this.singletonSub.isPaused) {
                        this.singletonSub.handler(this.obj, key, this.obj[key], oldValue);
                    }
                }
                catch (error) {
                    console.error(`ObjectSubject: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
                if (canCleanUpSubs) {
                    // If subscriptions were added during the notification, then singletonSub would be deleted and replaced with
                    // the subs array.
                    if (this.singletonSub) {
                        needCleanUpSubs = !this.singletonSub.isAlive;
                    }
                    else if (this.subs) {
                        for (let i = 0; i < this.subs.length; i++) {
                            if (!this.subs[i].isAlive) {
                                needCleanUpSubs = true;
                                break;
                            }
                        }
                    }
                }
            }
            else if (this.subs) {
                const subLen = this.subs.length;
                for (let i = 0; i < subLen; i++) {
                    try {
                        const sub = this.subs[i];
                        if (!sub.isPaused) {
                            sub.handler(this.obj, key, this.obj[key], oldValue);
                        }
                        needCleanUpSubs || (needCleanUpSubs = canCleanUpSubs && !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`ObjectSubject: error in handler: ${error}`);
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                // If subscriptions were added during the notification and a cleanup operation is not already pending, then we
                // need to check if any of the new subscriptions are already dead and if so, pend a cleanup operation.
                if (canCleanUpSubs && !needCleanUpSubs) {
                    for (let i = subLen; i < this.subs.length; i++) {
                        if (!this.subs[i].isAlive) {
                            needCleanUpSubs = true;
                            break;
                        }
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs) {
                if (this.singletonSub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    this.subs = this.subs.filter(sub => sub.isAlive);
                }
            }
        }
        /**
         * Notifies a subscription of this subject's current state.
         * @param sub The subscription to notify.
         */
        initialNotify(sub) {
            for (const key in this.obj) {
                const v = this.obj[key];
                sub.handler(this.obj, key, v, v);
            }
        }
        /**
         * Responds to when a subscription to this subscribable is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                if (this.singletonSub === sub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    const index = this.subs.indexOf(sub);
                    if (index >= 0) {
                        this.subs.splice(index, 1);
                    }
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        map(fn, equalityFunc, mutateFunc, initialVal) {
            const mapFunc = (inputs, previousVal) => fn(inputs[0], previousVal);
            return mutateFunc
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                ? MappedSubject.create(mapFunc, equalityFunc, mutateFunc, initialVal, this)
                : MappedSubject.create(mapFunc, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, this);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        pipe(to, arg2, arg3) {
            let sub;
            let paused;
            if (typeof arg2 === 'function') {
                sub = new SubscribablePipe(this, to, arg2, this.onSubDestroyedFunc);
                paused = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                sub = new SubscribablePipe(this, to, this.onSubDestroyedFunc);
                paused = arg2 !== null && arg2 !== void 0 ? arg2 : false;
            }
            this.subs.push(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
    }

    /**
     * Types of changes made to {@link SubscribableMap}.
     */
    exports.SubscribableMapEventType = void 0;
    (function (SubscribableMapEventType) {
        /** A key was added. */
        SubscribableMapEventType["Added"] = "Added";
        /** A key's entry was changed. */
        SubscribableMapEventType["Changed"] = "Changed";
        /** A key was deleted. */
        SubscribableMapEventType["Deleted"] = "Deleted";
    })(exports.SubscribableMapEventType || (exports.SubscribableMapEventType = {}));

    /* eslint-disable no-inner-declarations */
    /** A releative render position. */
    exports.RenderPosition = void 0;
    (function (RenderPosition) {
        RenderPosition[RenderPosition["Before"] = 0] = "Before";
        RenderPosition[RenderPosition["After"] = 1] = "After";
        RenderPosition[RenderPosition["In"] = 2] = "In";
    })(exports.RenderPosition || (exports.RenderPosition = {}));
    /**
     * A display component in the component framework.
     * @typedef P The type of properties for this component.
     * @typedef C The type of context that this component might have.
     */
    class DisplayComponent {
        /**
         * Creates an instance of a DisplayComponent.
         * @param props The propertis of the component.
         */
        constructor(props) {
            /** The context on this component, if any. */
            this.context = undefined;
            /** The type of context for this component, if any. */
            this.contextType = undefined;
            this.props = props;
        }
        /**
         * A callback that is called before the component is rendered.
         */
        onBeforeRender() { return; }
        /**
         * A callback that is called after the component is rendered.
         * @param node The component's VNode.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onAfterRender(node) { return; }
        /**
         * Destroys this component.
         */
        destroy() { return; }
        /**
         * Gets a context data subscription from the context collection.
         * @param context The context to get the subscription for.
         * @returns The requested context.
         * @throws An error if no data for the specified context type could be found.
         */
        getContext(context) {
            if (this.context !== undefined && this.contextType !== undefined) {
                const index = this.contextType.indexOf(context);
                return this.context[index];
            }
            throw new Error('Could not find the provided context type.');
        }
    }
    /**
     * Base properties for display components.
     */
    class ComponentProps {
    }
    /**
     * A reference to a component or element node.
     */
    class NodeReference {
        constructor() {
            /** The internal reference instance. */
            this._instance = null;
        }
        /**
         * The instance of the element or component.
         * @returns The instance of the element or component.
         */
        get instance() {
            if (this._instance !== null) {
                return this._instance;
            }
            throw new Error('Instance was null.');
        }
        /**
         * Sets the value of the instance.
         */
        set instance(val) {
            this._instance = val;
        }
        /**
         * Gets the instance, or null if the instance is not populated.
         * @returns The component or element instance.
         */
        getOrDefault() {
            return this._instance;
        }
    }
    /**
     * Provides a context of data that can be passed down to child components via a provider.
     */
    class Context {
        /**
         * Creates an instance of a Context.
         * @param defaultValue The default value of this context.
         */
        constructor(defaultValue) {
            this.defaultValue = defaultValue;
            /**
             * The provider component that can be set to a specific context value.
             * @param props The props of the provider component.
             * @returns A new context provider.
             */
            this.Provider = (props) => new ContextProvider(props, this);
        }
    }
    /**
     * A provider component that can be set to a specific context value.
     */
    class ContextProvider extends DisplayComponent {
        /**
         * Creates an instance of a ContextProvider.
         * @param props The props on the component.
         * @param parent The parent context instance for this provider.
         */
        constructor(props, parent) {
            super(props);
            this.parent = parent;
        }
        /** @inheritdoc */
        render() {
            var _a;
            const children = (_a = this.props.children) !== null && _a !== void 0 ? _a : [];
            return exports.FSComponent.buildComponent(exports.FSComponent.Fragment, this.props, ...children);
        }
    }
    /**
     * The FS component namespace.
     */
    // eslint-disable-next-line @typescript-eslint/no-namespace
    exports.FSComponent = void 0;
    (function (FSComponent) {
        /**
         * Valid SVG element tags.
         */
        const svgTags = {
            'circle': true,
            'clipPath': true,
            'color-profile': true,
            'cursor': true,
            'defs': true,
            'desc': true,
            'ellipse': true,
            'g': true,
            'image': true,
            'line': true,
            'linearGradient': true,
            'marker': true,
            'mask': true,
            'path': true,
            'pattern': true,
            'polygon': true,
            'polyline': true,
            'radialGradient': true,
            'rect': true,
            'stop': true,
            'svg': true,
            'text': true,
            'tspan': true,
        };
        /**
         * A fragment of existing elements with no specific root.
         * @param props The fragment properties.
         * @returns The fragment children.
         */
        function Fragment(props) {
            return props.children;
        }
        FSComponent.Fragment = Fragment;
        /**
         * Builds a JSX based FSComponent.
         * @param type The DOM element tag that will be built.
         * @param props The properties to apply to the DOM element.
         * @param children Any children of this DOM element.
         * @returns The JSX VNode for the component or element.
         */
        // eslint-disable-next-line no-inner-declarations
        function buildComponent(type, props, ...children) {
            let vnode = null;
            if (typeof type === 'string') {
                let element;
                if (svgTags[type] !== undefined) {
                    element = document.createElementNS('http://www.w3.org/2000/svg', type);
                }
                else {
                    element = document.createElement(type);
                }
                if (props !== null) {
                    for (const key in props) {
                        if (key === 'ref' && props.ref !== undefined) {
                            props.ref.instance = element;
                        }
                        else {
                            const prop = props[key];
                            if (key === 'class' && typeof prop === 'object' && 'isSubscribableSet' in prop) {
                                // Bind CSS classes to a subscribable set
                                prop.sub((set, eventType, modifiedKey) => {
                                    if (eventType === exports.SubscribableSetEventType.Added) {
                                        element.classList.add(modifiedKey);
                                    }
                                    else {
                                        element.classList.remove(modifiedKey);
                                    }
                                }, true);
                            }
                            else if (key === 'style' && typeof prop === 'object' && 'isSubscribableMap' in prop) {
                                // Bind CSS styles to a subscribable map.
                                prop.sub((map, eventType, modifiedKey, modifiedValue) => {
                                    switch (eventType) {
                                        case exports.SubscribableMapEventType.Added:
                                        case exports.SubscribableMapEventType.Changed:
                                            element.style.setProperty(modifiedKey, modifiedValue);
                                            break;
                                        case exports.SubscribableMapEventType.Deleted:
                                            element.style.setProperty(modifiedKey, null);
                                            break;
                                    }
                                }, true);
                            }
                            else if (typeof prop === 'object' && 'isSubscribable' in prop) {
                                if (key === 'style' && prop instanceof ObjectSubject) {
                                    // Bind CSS styles to an object subject.
                                    prop.sub((v, style, newValue) => {
                                        element.style.setProperty(style.toString(), newValue);
                                    }, true);
                                }
                                else {
                                    // Bind an attribute to a subscribable.
                                    const ns = getNamespaceToSetAttributeWith(key);
                                    if (ns !== null) {
                                        prop.sub((v) => {
                                            element.setAttributeNS(ns, key, v);
                                        }, true);
                                    }
                                    else {
                                        prop.sub((v) => {
                                            element.setAttribute(key, v);
                                        }, true);
                                    }
                                }
                            }
                            else if (key === 'class' && typeof prop === 'object') {
                                // Bind CSS classes to an object of key value pairs where the values can be boolean | Subscribable<boolean>
                                for (const className in prop) {
                                    if (className.trim().length === 0) {
                                        continue;
                                    }
                                    const value = prop[className];
                                    if (typeof value === 'object' && 'isSubscribable' in value) {
                                        value.sub((showClass) => {
                                            element.classList.toggle(className, !!showClass);
                                        }, true);
                                    }
                                    else {
                                        element.classList.toggle(className, !!value);
                                    }
                                }
                            }
                            else if (key === 'style' && typeof prop === 'object') {
                                // Bind styles to an object of key value pairs
                                for (const style in prop) {
                                    if (style.trim().length === 0) {
                                        continue;
                                    }
                                    const value = prop[style];
                                    if (typeof value === 'object' && 'isSubscribable' in value) {
                                        value.sub(newValue => {
                                            element.style.setProperty(style, newValue !== null && newValue !== void 0 ? newValue : '');
                                        }, true);
                                    }
                                    else {
                                        element.style.setProperty(style, value !== null && value !== void 0 ? value : '');
                                    }
                                }
                            }
                            else {
                                const ns = getNamespaceToSetAttributeWith(key);
                                if (ns !== null) {
                                    element.setAttributeNS(ns, key, prop);
                                }
                                else {
                                    element.setAttribute(key, prop);
                                }
                            }
                        }
                    }
                }
                vnode = {
                    instance: element,
                    props: props,
                    children: null
                };
                vnode.children = createChildNodes(vnode, children);
            }
            else if (typeof type === 'function') {
                if (children !== null && props === null) {
                    props = {
                        children: children
                    };
                }
                else if (props !== null) {
                    props.children = children;
                }
                if (typeof type === 'function' && type.name === Fragment.name) {
                    let fragmentChildren = type(props);
                    //Handle the case where the single fragment children is an array of nodes passsed down from above
                    while (fragmentChildren && fragmentChildren.length === 1 && Array.isArray(fragmentChildren[0])) {
                        fragmentChildren = fragmentChildren[0];
                    }
                    vnode = {
                        instance: null,
                        props,
                        children: null
                    };
                    if (fragmentChildren) {
                        vnode.children = createChildNodes(vnode, fragmentChildren);
                    }
                }
                else {
                    let instance;
                    const pluginSystem = (window._pluginSystem);
                    try {
                        instance = type(props);
                    }
                    catch (_a) {
                        let pluginInstance = undefined;
                        if (pluginSystem !== undefined) {
                            pluginInstance = pluginSystem.onComponentCreating(type, props);
                        }
                        if (pluginInstance !== undefined) {
                            instance = pluginInstance;
                        }
                        else {
                            instance = new type(props);
                        }
                    }
                    if (props !== null && props.ref !== null && props.ref !== undefined) {
                        props.ref.instance = instance;
                    }
                    if (instance.contextType !== undefined) {
                        instance.context = instance.contextType.map(c => Subject.create(c.defaultValue));
                    }
                    if (pluginSystem !== undefined) {
                        pluginSystem.onComponentCreated(instance);
                    }
                    vnode = {
                        instance,
                        props,
                        children: [instance.render()]
                    };
                }
            }
            return vnode;
        }
        FSComponent.buildComponent = buildComponent;
        /**
         * Returns the namespace to use for setting an attribute, or null if none
         *
         * @param attribute the attribute to set
         *
         * @returns a string or null
         */
        function getNamespaceToSetAttributeWith(attribute) {
            if (attribute.startsWith('xlink:')) {
                return 'http://www.w3.org/1999/xlink';
            }
            return null;
        }
        /**
         * Creates the collection of child VNodes.
         * @param parent The parent VNode.
         * @param children The JSX children to convert to nodes.
         * @returns A collection of child VNodes.
         */
        function createChildNodes(parent, children) {
            let vnodes = null;
            if (children !== null && children !== undefined && children.length > 0) {
                vnodes = [];
                for (const child of children) {
                    if (child !== null) {
                        if (child instanceof Array) {
                            const arrayNodes = createChildNodes(parent, child);
                            if (arrayNodes !== null) {
                                vnodes.push(...arrayNodes);
                            }
                        }
                        else if (typeof child === 'object') {
                            if ('isSubscribable' in child) {
                                const node = {
                                    instance: child,
                                    children: null,
                                    props: null,
                                    root: undefined,
                                };
                                child.sub((v) => {
                                    if (node.root !== undefined) {
                                        // TODO workaround. gotta find a solution for the text node vanishing when text is empty
                                        node.root.nodeValue = (v === '' || v === null || v === undefined)
                                            ? ' '
                                            : v.toString();
                                    }
                                });
                                vnodes.push(node);
                            }
                            else {
                                vnodes.push(child);
                            }
                        }
                        else if (typeof child === 'string' || typeof child === 'number') {
                            vnodes.push(createStaticContentNode(child));
                        }
                    }
                }
            }
            return vnodes;
        }
        FSComponent.createChildNodes = createChildNodes;
        /**
         * Creates a static content VNode.
         * @param content The content to create a node for.
         * @returns A static content VNode.
         */
        function createStaticContentNode(content) {
            return {
                instance: content,
                children: null,
                props: null
            };
        }
        FSComponent.createStaticContentNode = createStaticContentNode;
        /**
         * Renders a VNode to a DOM element.
         * @param node The node to render.
         * @param element The DOM element to render to.
         * @param position The RenderPosition to put the item in.
         */
        function render(node, element, position = exports.RenderPosition.In) {
            if (node.instance instanceof HTMLElement || node.instance instanceof SVGElement) {
                if (element !== null) {
                    insertNode(node, position, element);
                }
            }
            else if (node.children && node.children.length > 0 && element !== null) {
                const componentInstance = node.instance;
                if (componentInstance !== null && componentInstance.onBeforeRender !== undefined) {
                    componentInstance.onBeforeRender();
                }
                if (position === exports.RenderPosition.After) {
                    for (let i = node.children.length - 1; i >= 0; i--) {
                        if (node.children[i] === undefined || node.children[i] === null) {
                            continue;
                        }
                        insertNode(node.children[i], position, element);
                    }
                }
                else {
                    for (let i = 0; i < node.children.length; i++) {
                        if (node.children[i] === undefined || node.children[i] === null) {
                            continue;
                        }
                        insertNode(node.children[i], position, element);
                    }
                }
                const instance = node.instance;
                if (instance instanceof ContextProvider) {
                    visitNodes(node, (n) => {
                        if (n === undefined || n === null) {
                            return false;
                        }
                        const nodeInstance = n.instance;
                        if (nodeInstance !== null && nodeInstance.contextType !== undefined) {
                            const contextSlot = nodeInstance.contextType.indexOf(instance.parent);
                            if (contextSlot >= 0) {
                                if (nodeInstance.context === undefined) {
                                    nodeInstance.context = [];
                                }
                                nodeInstance.context[contextSlot].set(instance.props.value);
                            }
                            if (nodeInstance instanceof ContextProvider && nodeInstance !== instance && nodeInstance.parent === instance.parent) {
                                return true;
                            }
                        }
                        return false;
                    });
                }
                if (componentInstance !== null && componentInstance.onAfterRender !== undefined) {
                    const pluginSystem = (window._pluginSystem);
                    componentInstance.onAfterRender(node);
                    if (pluginSystem !== undefined) {
                        pluginSystem.onComponentRendered(node);
                    }
                }
            }
        }
        FSComponent.render = render;
        /**
         * Inserts a node into the DOM.
         * @param node The node to insert.
         * @param position The position to insert the node in.
         * @param element The element to insert relative to.
         */
        function insertNode(node, position, element) {
            var _a, _b, _c, _d, _e, _f;
            if (node.instance instanceof HTMLElement || node.instance instanceof SVGElement) {
                switch (position) {
                    case exports.RenderPosition.In:
                        element.appendChild(node.instance);
                        node.root = (_a = element.lastChild) !== null && _a !== void 0 ? _a : undefined;
                        break;
                    case exports.RenderPosition.Before:
                        element.insertAdjacentElement('beforebegin', node.instance);
                        node.root = (_b = element.previousSibling) !== null && _b !== void 0 ? _b : undefined;
                        break;
                    case exports.RenderPosition.After:
                        element.insertAdjacentElement('afterend', node.instance);
                        node.root = (_c = element.nextSibling) !== null && _c !== void 0 ? _c : undefined;
                        break;
                }
                if (node.children !== null) {
                    for (const child of node.children) {
                        insertNode(child, exports.RenderPosition.In, node.instance);
                    }
                }
            }
            else if (typeof node.instance === 'string'
                || (typeof node.instance === 'object'
                    && node.instance !== null &&
                    'isSubscribable' in node.instance)) {
                let toRender;
                if (typeof node.instance === 'string') {
                    toRender = node.instance;
                }
                else {
                    toRender = node.instance.get();
                    if (toRender === '') {
                        toRender = ' '; // prevent disappearing text node
                    }
                }
                switch (position) {
                    case exports.RenderPosition.In:
                        element.insertAdjacentHTML('beforeend', toRender);
                        node.root = (_d = element.lastChild) !== null && _d !== void 0 ? _d : undefined;
                        break;
                    case exports.RenderPosition.Before:
                        element.insertAdjacentHTML('beforebegin', toRender);
                        node.root = (_e = element.previousSibling) !== null && _e !== void 0 ? _e : undefined;
                        break;
                    case exports.RenderPosition.After:
                        element.insertAdjacentHTML('afterend', toRender);
                        node.root = (_f = element.nextSibling) !== null && _f !== void 0 ? _f : undefined;
                        break;
                }
            }
            else if (Array.isArray(node)) {
                if (position === exports.RenderPosition.After) {
                    for (let i = node.length - 1; i >= 0; i--) {
                        render(node[i], element, position);
                    }
                }
                else {
                    for (let i = 0; i < node.length; i++) {
                        render(node[i], element, position);
                    }
                }
            }
            else {
                render(node, element, position);
            }
        }
        /**
         * Render a node before a DOM element.
         * @param node The node to render.
         * @param element The element to render boeore.
         */
        function renderBefore(node, element) {
            render(node, element, exports.RenderPosition.Before);
        }
        FSComponent.renderBefore = renderBefore;
        /**
         * Render a node after a DOM element.
         * @param node The node to render.
         * @param element The element to render after.
         */
        function renderAfter(node, element) {
            render(node, element, exports.RenderPosition.After);
        }
        FSComponent.renderAfter = renderAfter;
        /**
         * Remove a previously rendered element.  Currently, this is just a simple
         * wrapper so that all of our high-level "component maniuplation" state is kept
         * in the FSComponent API, but it's not doing anything other than a simple
         * remove() on the element.   This can probably be enhanced.
         * @param element The element to remove.
         */
        function remove(element) {
            if (element !== null) {
                element.remove();
            }
        }
        FSComponent.remove = remove;
        /**
         * Creates a component or element node reference.
         * @returns A new component or element node reference.
         */
        function createRef() {
            return new NodeReference();
        }
        FSComponent.createRef = createRef;
        /**
         * Creates a new context to hold data for passing to child components.
         * @param defaultValue The default value of this context.
         * @returns A new context.
         */
        function createContext(defaultValue) {
            return new Context(defaultValue);
        }
        FSComponent.createContext = createContext;
        /**
         * Visits VNodes with a supplied visitor function within the given children tree.
         * @param node The node to visit.
         * @param visitor The visitor function to inspect VNodes with. Return true if the search should stop at the visited
         * node and not proceed any further down the node's children.
         */
        function visitNodes(node, visitor) {
            if (node === undefined || node === null) {
                return;
            }
            const stopVisitation = visitor(node);
            if (!stopVisitation && node.children !== undefined && node.children !== null) {
                for (let i = 0; i < node.children.length; i++) {
                    const child = node.children[i];
                    if (Array.isArray(child)) {
                        for (let childIndex = 0; childIndex < child.length; childIndex++) {
                            visitNodes(child[childIndex], visitor);
                        }
                    }
                    else {
                        visitNodes(child, visitor);
                    }
                }
            }
            return;
        }
        FSComponent.visitNodes = visitNodes;
        /**
         * Parses a space-delimited CSS class string into an array of CSS classes.
         * @param classString A space-delimited CSS class string.
         * @param filter A function which filters parsed classes. For each class, the function should return `true` if the
         * class should be included in the output array and `false` otherwise.
         * @returns An array of CSS classes derived from the specified CSS class string.
         */
        function parseCssClassesFromString(classString, filter) {
            return classString.split(' ').filter(str => str !== '' && (filter === undefined || filter(str)));
        }
        FSComponent.parseCssClassesFromString = parseCssClassesFromString;
        // eslint-disable-next-line jsdoc/require-jsdoc
        function bindCssClassSet(setToBind, classesToSubscribe, reservedClasses) {
            const reservedClassSet = new Set(reservedClasses);
            if (classesToSubscribe.isSubscribableSet === true) {
                return bindCssClassSetToSubscribableSet(setToBind, classesToSubscribe, reservedClassSet);
            }
            else {
                return bindCssClassSetToRecord(setToBind, classesToSubscribe, reservedClassSet);
            }
        }
        FSComponent.bindCssClassSet = bindCssClassSet;
        /**
         * Binds a {@link MutableSubscribableSet} to a subscribable set of CSS classes. CSS classes added to and removed from
         * the subscribed set will also be added to and removed from the bound set, with the exception of a set of reserved
         * classes. The presence or absence of any of the reserved classes in the bound set is not affected by the subscribed
         * set.
         * @param setToBind The set to bind.
         * @param classesToSubscribe A set of CSS classes to which to subscribe.
         * @param reservedClassSet A set of reserved classes.
         * @returns The newly created subscription to the subscribed CSS class set.
         */
        function bindCssClassSetToSubscribableSet(setToBind, classesToSubscribe, reservedClassSet) {
            if (reservedClassSet.size === 0) {
                return classesToSubscribe.sub((set, type, key) => {
                    if (type === exports.SubscribableSetEventType.Added) {
                        setToBind.add(key);
                    }
                    else {
                        setToBind.delete(key);
                    }
                }, true);
            }
            else {
                return classesToSubscribe.sub((set, type, key) => {
                    if (reservedClassSet.has(key)) {
                        return;
                    }
                    if (type === exports.SubscribableSetEventType.Added) {
                        setToBind.add(key);
                    }
                    else {
                        setToBind.delete(key);
                    }
                }, true);
            }
        }
        /**
         * Binds a {@link MutableSubscribableSet} to a record of CSS classes. CSS classes toggled in the record will also be
         * added to and removed from the bound set, with the exception of a set of reserved classes. The presence or absence
         * of any of the reserved classes in the bound set is not affected by the subscribed record.
         * @param setToBind The set to bind.
         * @param classesToSubscribe A record of CSS classes to which to subscribe.
         * @param reservedClassSet A set of reserved classes.
         * @returns The newly created subscriptions to the CSS class record.
         */
        function bindCssClassSetToRecord(setToBind, classesToSubscribe, reservedClassSet) {
            const subs = [];
            for (const cssClass in classesToSubscribe) {
                if (reservedClassSet.has(cssClass)) {
                    continue;
                }
                const value = classesToSubscribe[cssClass];
                if (typeof value === 'object') {
                    subs.push(value.sub(setToBind.toggle.bind(setToBind, cssClass), true));
                }
                else if (value === true) {
                    setToBind.add(cssClass);
                }
                else {
                    setToBind.delete(cssClass);
                }
            }
            return subs;
        }
        /**
         * Adds CSS classes to a {@link ToggleableClassNameRecord}.
         * @param record The CSS class record to which to add the new classes. The record will be mutated as classes are
         * added.
         * @param classesToAdd The CSS classes to add to the record, as a space-delimited class string, an iterable of
         * individual class names, or a {@link ToggleableClassNameRecord}.
         * @param allowOverwrite Whether to allow the new classes to overwrite existing entries in the CSS class record.
         * Defaults to `true`.
         * @param filter A function which filters the classes to add. For each class, the function should return `true` if
         * the class should be included in the record and `false` otherwise.
         * @returns The mutated CSS class record, after the new classes have been added.
         */
        function addCssClassesToRecord(record, classesToAdd, allowOverwrite = true, filter) {
            if (classesToAdd === '') {
                return record;
            }
            if (typeof classesToAdd === 'string') {
                classesToAdd = FSComponent.parseCssClassesFromString(classesToAdd, filter);
                filter = undefined;
            }
            if (typeof classesToAdd[Symbol.iterator] === 'function') {
                for (const cssClass of classesToAdd) {
                    if ((allowOverwrite || record[cssClass] === undefined) && (!filter || filter(cssClass))) {
                        record[cssClass] = true;
                    }
                }
            }
            else {
                for (const cssClass in classesToAdd) {
                    if ((allowOverwrite || record[cssClass] === undefined) && (!filter || filter(cssClass))) {
                        record[cssClass] = classesToAdd[cssClass];
                    }
                }
            }
            return record;
        }
        FSComponent.addCssClassesToRecord = addCssClassesToRecord;
        // eslint-disable-next-line jsdoc/require-jsdoc
        function bindStyleMap(mapToBind, stylesToSubscribe, reservedStyles) {
            const reservedStyleSet = new Set(reservedStyles);
            if (stylesToSubscribe.isSubscribableMap === true) {
                return bindStyleMapToSubscribableMap(mapToBind, stylesToSubscribe, reservedStyleSet);
            }
            else if (stylesToSubscribe instanceof ObjectSubject) {
                return bindStyleMapToObjectSubject(mapToBind, stylesToSubscribe, reservedStyleSet);
            }
            else {
                return bindStyleMapToRecord(mapToBind, stylesToSubscribe, reservedStyleSet);
            }
        }
        FSComponent.bindStyleMap = bindStyleMap;
        /**
         * Binds a {@link MutableSubscribableMap} to a subscribable map of CSS styles. Modifications to the CSS styles in the
         * subscribed map will be reflected in the bound map, with the exception of a set of reserved styles. The values of
         * any of the reserved styles in the bound map is not affected by the subscribed map.
         * @param mapToBind The map to bind.
         * @param stylesToSubscribe A key-value map of CSS styles to which to subscribe.
         * @param reservedStyleSet A set of reserved styles.
         * @returns The newly created subscription to the subscribed CSS style map.
         */
        function bindStyleMapToSubscribableMap(mapToBind, stylesToSubscribe, reservedStyleSet) {
            if (reservedStyleSet.size === 0) {
                return stylesToSubscribe.pipe(mapToBind);
            }
            else {
                return stylesToSubscribe.sub((set, type, key, value) => {
                    if (reservedStyleSet.has(key)) {
                        return;
                    }
                    switch (type) {
                        case exports.SubscribableMapEventType.Added:
                        case exports.SubscribableMapEventType.Changed:
                            mapToBind.setValue(key, value);
                            break;
                        case exports.SubscribableMapEventType.Deleted:
                            mapToBind.delete(key);
                            break;
                    }
                }, true);
            }
        }
        /**
         * Binds a {@link MutableSubscribableMap} to an {@link ObjectSubject} of CSS styles. Modifications to the CSS styles
         * in the subject will be reflected in the bound map, with the exception of a set of reserved styles. The values of
         * any of the reserved styles in the bound map is not affected by the subscribed subject.
         * @param mapToBind The map to bind.
         * @param stylesToSubscribe An ObjectSubject of CSS styles to which to subscribe.
         * @param reservedStyleSet A set of reserved styles.
         * @returns The newly created subscription to the CSS style ObjectSubject.
         */
        function bindStyleMapToObjectSubject(mapToBind, stylesToSubscribe, reservedStyleSet) {
            return stylesToSubscribe.sub((obj, style, value) => {
                if (reservedStyleSet.has(style)) {
                    return;
                }
                if (value) {
                    mapToBind.setValue(style, value);
                }
                else {
                    mapToBind.delete(style);
                }
            }, true);
        }
        /**
         * Binds a {@link MutableSubscribableMap} to a record of CSS styles. Modifications to the CSS styles in the record
         * will be reflected in the bound map, with the exception of a set of reserved styles. The values of any of the
         * reserved styles in the bound map is not affected by the subscribed record.
         * @param mapToBind The map to bind.
         * @param stylesToSubscribe A record of CSS styles to which to subscribe.
         * @param reservedStyleSet A set of reserved styles.
         * @returns The newly created subscriptions to the CSS style record.
         */
        function bindStyleMapToRecord(mapToBind, stylesToSubscribe, reservedStyleSet) {
            const subs = [];
            for (const style in stylesToSubscribe) {
                if (reservedStyleSet.has(style)) {
                    continue;
                }
                const value = stylesToSubscribe[style];
                if (typeof value === 'object') {
                    subs.push(value.sub(styleValue => {
                        if (styleValue) {
                            mapToBind.setValue(style, styleValue);
                        }
                        else {
                            mapToBind.delete(style);
                        }
                    }, true));
                }
                else if (value) {
                    mapToBind.setValue(style, value);
                }
                else {
                    mapToBind.delete(style);
                }
            }
            return subs;
        }
        /**
         * Traverses a VNode tree in depth-first order and destroys the first {@link DisplayComponent} encountered in each
         * branch of the tree.
         * @param root The root of the tree to traverse.
         */
        function shallowDestroy(root) {
            FSComponent.visitNodes(root, node => {
                if (node !== root && node.instance instanceof DisplayComponent) {
                    node.instance.destroy();
                    return true;
                }
                return false;
            });
        }
        FSComponent.shallowDestroy = shallowDestroy;
        /**
         * An empty callback handler.
         */
        FSComponent.EmptyHandler = () => { return; };
    })(exports.FSComponent || (exports.FSComponent = {}));
    /**
     * A system that handles the registration and boostrapping of plugin scripts.
     */
    class PluginSystem {
        /**
         * Ctor
         */
        constructor() {
            this.scripts = [];
            this.plugins = [];
            /** An event subscribable that publishes when a new component is about to be created. */
            this.creatingHandlers = [];
            /** An event subscribable that publishes when a new component is created. */
            this.createdHandlers = [];
            /** An event subscribable that publishes when a component has finished rendering. */
            this.renderedHandlers = [];
            this.initViewListenerPromise = new Promise((resolve) => {
                this.pluginViewListener = RegisterViewListener('JS_LISTENER_PLUGINS', () => {
                    resolve();
                });
            });
        }
        /**
         * Adds plugin scripts to load to the system.
         * @param document The panel.xml document to load scripts from.
         * @param instrumentId The ID of the instrument.
         * @param globalPluginTargetFunc A function that returns true if a global plugin should be loaded.
         * @example
         * await this.pluginSystem.addScripts(this.instrument.xmlConfig, this.instrument.templateID, (target) => {
         *   return target === this.instrument.templateID;
         * });
         */
        async addScripts(document, instrumentId, globalPluginTargetFunc) {
            let pluginTags = undefined;
            await this.initViewListenerPromise;
            // check if the listener exists
            if (typeof PluginsListener !== 'undefined') {
                // wait for init of the plugin viewlistener
                if (this.pluginViewListener !== undefined) {
                    // get global plugins
                    const pluginsResp = await this.pluginViewListener.call('GET_PLUGINS');
                    const globalPlugins = pluginsResp;
                    for (let i = 0; i < globalPlugins.plugins.length; i++) {
                        const plugin = globalPlugins.plugins[i];
                        if (globalPluginTargetFunc(plugin.target) === true) {
                            const scriptUri = plugin.path;
                            if (scriptUri !== null) {
                                this.scripts.push(scriptUri);
                            }
                        }
                    }
                }
            }
            // get from panel.xml
            const instrumentConfigs = document.getElementsByTagName('Instrument');
            for (let i = 0; i < instrumentConfigs.length; i++) {
                const el = instrumentConfigs.item(i);
                if (el !== null) {
                    const nameEl = el.getElementsByTagName('Name');
                    if (nameEl.length > 0 && nameEl[0].textContent === instrumentId) {
                        pluginTags = el.getElementsByTagName('Plugin');
                    }
                }
            }
            if (pluginTags !== undefined) {
                for (let i = 0; i < pluginTags.length; i++) {
                    const scriptUri = pluginTags[i].textContent;
                    if (scriptUri !== null) {
                        this.scripts.push(scriptUri);
                    }
                }
            }
        }
        /**
         * Starts the plugin system with the included avionics specific plugin binder.
         * @param binder The plugin binder to pass to the individual plugins.
         */
        async startSystem(binder) {
            window._pluginSystem = this;
            this.binder = binder;
            const loadPromises = [];
            for (const script of this.scripts) {
                const scriptTag = document.createElement('script');
                scriptTag.src = script;
                scriptTag.async = false;
                document.head.append(scriptTag);
                loadPromises.push(new Promise((resolve, reject) => {
                    scriptTag.onload = () => resolve();
                    scriptTag.onerror = (ev) => reject(ev);
                }).catch(e => console.error(e)));
            }
            await Promise.all(loadPromises).then(() => {
                for (const plugin of this.plugins) {
                    plugin.onInstalled();
                }
            });
        }
        /**
         * Adds a plugin to the plugin system.
         * @param plugin The plugin to add.
         */
        addPlugin(plugin) {
            this.plugins.push(plugin);
        }
        /**
         * Runs the provided function on all of the registered plugins.
         * @param fun The function to run.
         * @param reverseOrder Whether to iterate through plugins in reverse order. Defaults to `false`.
         */
        callPlugins(fun, reverseOrder = false) {
            if (reverseOrder) {
                for (let i = this.plugins.length - 1; i >= 0; i--) {
                    fun(this.plugins[i]);
                }
            }
            else {
                for (let i = 0; i < this.plugins.length; i++) {
                    fun(this.plugins[i]);
                }
            }
        }
        /**
         * Subscribes a handler to the component creating hook.
         * @param handler The handler to subscribe.
         */
        subscribeOnComponentCreating(handler) {
            this.creatingHandlers.push(handler);
        }
        /**
         * A hook that allows plugins to replace components that are about to be created with their own implementations.
         * @param constructor The display component constructor that is going to be used.
         * @param props The component props that will be passed into the component.
         * @returns Returns either the display component that will replace, or undefined if the component should not be replaced.
         */
        onComponentCreating(constructor, props) {
            let component = undefined;
            for (let i = 0; i < this.creatingHandlers.length; i++) {
                component = this.creatingHandlers[i](constructor, props);
                if (component !== undefined) {
                    return component;
                }
            }
            return undefined;
        }
        /**
         * Subscribes a handler to the component created hook.
         * @param handler The handler to subscribe.
         */
        subscribeOnComponentCreated(handler) {
            this.createdHandlers.push(handler);
        }
        /**
         * A hook that allows plugins to observe components as they are created.
         * @param component The component that was created.
         */
        onComponentCreated(component) {
            for (let i = 0; i < this.createdHandlers.length; i++) {
                this.createdHandlers[i](component);
            }
        }
        /**
         * Subscribes a handler to the component rendered hook.
         * @param handler The handler to subscribe.
         */
        subscribeOnComponentRendered(handler) {
            this.renderedHandlers.push(handler);
        }
        /**
         * A hook that allows plugins to observe built VNodes after they are rendered.
         * @param node The node that was rendered.
         */
        onComponentRendered(node) {
            for (let i = 0; i < this.renderedHandlers.length; i++) {
                this.renderedHandlers[i](node);
            }
        }
    }
    /**
     * A plugin that is created and managed by the plugin system.
     */
    class AvionicsPlugin {
        /**
         * Creates an instance of a Plugin.
         * @param binder The avionics specific plugin binder to accept from the system.
         */
        constructor(binder) {
            this.binder = binder;
        }
        /**
         * Loads a CSS file into the instrument.
         * @param uri The URI to the CSS file.
         */
        async loadCss(uri) {
            const linkTag = document.createElement('link');
            linkTag.rel = 'stylesheet';
            linkTag.href = uri;
            document.head.append(linkTag);
            return new Promise((resolve) => {
                linkTag.onload = () => resolve();
            });
        }
    }
    /**
     * Registers a plugin with the plugin system.
     * @param plugin The plugin to register.
     */
    function registerPlugin(plugin) {
        const pluginSystem = window._pluginSystem;
        if (pluginSystem.binder !== undefined) {
            const instance = new plugin(pluginSystem.binder);
            pluginSystem.addPlugin(instance);
            if (instance.onComponentCreating !== undefined) {
                pluginSystem.subscribeOnComponentCreating(instance.onComponentCreating);
            }
            if (instance.onComponentCreated !== undefined) {
                pluginSystem.subscribeOnComponentCreated(instance.onComponentCreated);
            }
            if (instance.onComponentRendered !== undefined) {
                pluginSystem.subscribeOnComponentRendered(instance.onComponentRendered);
            }
        }
    }
    const Fragment = exports.FSComponent.Fragment;

    /// <reference types="@microsoft/msfs-types/js/common" />
    /// <reference types="@microsoft/msfs-types/js/types" />
    /// <reference types="@microsoft/msfs-types/js/netbingmap" />
    /**
     * A FSComponent that displays the MSFS Bing Map, weather radar, and 3D terrain.
     */
    class BingComponent extends DisplayComponent {
        constructor() {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            super(...arguments);
            this.bingFlags = this.getBingFlags(EBingReference.SEA);
            this.isListenerRegistered = false;
            this.imgRef = exports.FSComponent.createRef();
            this.uid = 0;
            this._isBound = false;
            this._isAwake = true;
            this.isDestroyed = false;
            this.pos = new LatLong(0, 0);
            this.radius = 10;
            this.resolution = (_a = this.props.resolution) !== null && _a !== void 0 ? _a : Vec2Subject.create(Vec2Math.create(BingComponent.DEFAULT_RESOLUTION, BingComponent.DEFAULT_RESOLUTION));
            this.earthColors = (_b = this.props.earthColors) !== null && _b !== void 0 ? _b : ArraySubject.create(ArrayUtils.create(2, () => BingComponent.hexaToRGBColor('#000000')));
            this.earthColorsElevationRange = (_c = this.props.earthColorsElevationRange) !== null && _c !== void 0 ? _c : Vec2Subject.create(Vec2Math.create(0, 30000));
            this.skyColor = (_d = this.props.skyColor) !== null && _d !== void 0 ? _d : Subject.create(BingComponent.hexaToRGBColor('#000000'));
            this.reference = (_e = this.props.reference) !== null && _e !== void 0 ? _e : Subject.create(EBingReference.SEA);
            this.wxrMode = (_f = this.props.wxrMode) !== null && _f !== void 0 ? _f : Subject.create({ mode: EWeatherRadar.OFF, arcRadians: 0.5 });
            this.wxrColors = (_g = this.props.wxrColors) !== null && _g !== void 0 ? _g : ArraySubject.create(Array.from(BingComponent.DEFAULT_WEATHER_COLORS));
            this.isoLines = (_h = this.props.isoLines) !== null && _h !== void 0 ? _h : Subject.create(false);
            this.wxrColorsArray = [];
            this.wxrRateArray = [];
            this.resolutionHandler = (resolution) => {
                Coherent.call('SET_MAP_RESOLUTION', this.uid, resolution[0], resolution[1]);
                // The sim ignores position/radius updates within a certain number of frames of sending a resolution change, so we
                // will keep trying to send pending updates for a few frames after any resolution change.
                this.positionRadiusInhibitFramesRemaining = BingComponent.POSITION_RADIUS_INHIBIT_FRAMES;
                if (!this.positionRadiusInhibitTimer.isPending()) {
                    this.positionRadiusInhibitTimer.schedule(this.processPendingPositionRadius, 0);
                }
            };
            this.earthColorsHandler = () => {
                const colors = this.earthColors.getArray();
                if (colors.length < 2) {
                    return;
                }
                Coherent.call('SET_MAP_HEIGHT_COLORS', this.uid, colors);
            };
            this.earthColorsElevationRangeHandler = () => {
                const colors = this.earthColors.getArray();
                if (colors.length < 2) {
                    return;
                }
                // The way the map assigns colors to elevations is as follows:
                // ----------------------------------------------------------------------------------
                // - altitude range = MIN to MAX
                // - colors = array of length N >= 2 (colors[0] is the water color)
                // - STEP = (MAX - MIN) / N
                // - colors[i] is assigned to elevations from MIN + STEP * i to MIN + STEP * (i + 1)
                // - colors[1] is also assigned to all elevations < MIN + STEP
                // - colors[N - 1] is also assigned to all elevations > MIN + STEP * N
                // ----------------------------------------------------------------------------------
                const range = this.earthColorsElevationRange.get();
                const terrainColorCount = colors.length - 1;
                const desiredElevationStep = (range[1] - range[0]) / Math.max(terrainColorCount - 1, 1);
                const requiredMin = range[0] - desiredElevationStep;
                const requiredMax = range[1] + desiredElevationStep;
                Coherent.call('SET_MAP_ALTITUDE_RANGE', this.uid, requiredMin, requiredMax);
            };
            this.skyColorHandler = (color) => {
                Coherent.call('SET_MAP_CLEAR_COLOR', this.uid, color);
            };
            this.referenceHandler = (reference) => {
                this.bingFlags = this.getBingFlags(reference);
                this.mapListener.trigger('JS_BIND_BINGMAP', this.props.id, this.bingFlags);
            };
            this.wxrModeHandler = (wxrMode) => {
                Coherent.call('SHOW_MAP_WEATHER', this.uid, wxrMode.mode, wxrMode.arcRadians);
            };
            this.wxrColorsHandler = () => {
                const array = this.wxrColors.getArray();
                if (array.length === 0) {
                    return;
                }
                this.wxrColorsArray.length = array.length;
                this.wxrRateArray.length = array.length;
                for (let i = 0; i < array.length; i++) {
                    this.wxrColorsArray[i] = array[i][0];
                    this.wxrRateArray[i] = array[i][1];
                }
                Coherent.call('SET_MAP_WEATHER_RADAR_COLORS', this.uid, this.wxrColorsArray, this.wxrRateArray);
            };
            this.isoLinesHandler = (showIsolines) => {
                Coherent.call('SHOW_MAP_ISOLINES', this.uid, showIsolines);
            };
            this.setCurrentMapParamsTimer = null;
            this.positionRadiusInhibitFramesRemaining = 0;
            this.isPositionRadiusPending = false;
            this.positionRadiusInhibitTimer = new DebounceTimer();
            this.processPendingPositionRadius = () => {
                if (this.isPositionRadiusPending) {
                    Coherent.call('SET_MAP_PARAMS', this.uid, this.pos, this.radius);
                }
                if (--this.positionRadiusInhibitFramesRemaining > 0) {
                    this.positionRadiusInhibitTimer.schedule(this.processPendingPositionRadius, 0);
                }
                else {
                    this.isPositionRadiusPending = false;
                }
            };
            /**
             * A callback called when the listener is fully bound.
             * @param binder The binder from the listener.
             * @param uid The unique ID of the bound map.
             */
            this.onMapBound = (binder, uid) => {
                if (this.isDestroyed) {
                    return;
                }
                if (binder.friendlyName === this.props.id) {
                    this.binder = binder;
                    this.uid = uid;
                    if (this._isBound) {
                        return;
                    }
                    this._isBound = true;
                    Coherent.call('SHOW_MAP', uid, true);
                    const pause = !this._isAwake;
                    this.earthColorsSub = this.earthColors.sub(() => {
                        this.earthColorsHandler();
                        this.earthColorsElevationRangeHandler();
                    }, true, pause);
                    this.earthColorsElevationRangeSub = this.earthColorsElevationRange.sub(this.earthColorsElevationRangeHandler, true, pause);
                    this.skyColorSub = this.skyColor.sub(this.skyColorHandler, true, pause);
                    this.referenceSub = this.reference.sub(this.referenceHandler, true, pause);
                    this.wxrModeSub = this.wxrMode.sub(this.wxrModeHandler, true, pause);
                    this.wxrColorsSub = this.wxrColors.sub(this.wxrColorsHandler, true, pause);
                    this.resolutionSub = this.resolution.sub(this.resolutionHandler, true, pause);
                    this.isoLinesSub = this.isoLines.sub(this.isoLinesHandler, true, pause);
                    // Only when not SVT, send in initial map params (even if we are asleep), because a bing instance that doesn't
                    // have params initialized causes GPU perf issues.
                    if (!BitFlags.isAll(this.bingFlags, BingMapsFlags.FL_BINGMAP_3D)) {
                        Coherent.call('SET_MAP_PARAMS', this.uid, this.pos, this.radius);
                    }
                    this.props.onBoundCallback && this.props.onBoundCallback(this);
                }
            };
            /**
             * A callback called when the map image is updated.
             * @param uid The unique ID of the bound map.
             * @param imgSrc The img tag src attribute to assign to the bing map image.
             */
            this.onMapUpdate = (uid, imgSrc) => {
                if (this.binder !== undefined && this.uid === uid && this.imgRef.instance !== null) {
                    if (this.imgRef.instance.src !== imgSrc) {
                        this.imgRef.instance.src = imgSrc;
                    }
                }
            };
            /**
             * Calls the position and radius set function to set map parameters.
             */
            this.setCurrentMapParams = () => {
                this.setPositionRadius(this.pos, this.radius);
            };
        }
        /**
         * Checks whether this Bing component has been bound.
         * @returns whether this Bing component has been bound.
         */
        isBound() {
            return this._isBound;
        }
        /**
         * Checks whether this Bing component is awake.
         * @returns whether this Bing component is awake.
         */
        isAwake() {
            return this._isAwake;
        }
        /** @inheritdoc */
        onAfterRender() {
            if (window['IsDestroying']) {
                this.destroy();
                return;
            }
            const gameStateSubscribable = GameStateProvider.get();
            const gameState = gameStateSubscribable.get();
            if (gameState === GameState.briefing || gameState === GameState.ingame) {
                this.registerListener();
            }
            else {
                this.gameStateSub = gameStateSubscribable.sub(state => {
                    var _a;
                    if (this.isDestroyed) {
                        return;
                    }
                    if (state === GameState.briefing || state === GameState.ingame) {
                        (_a = this.gameStateSub) === null || _a === void 0 ? void 0 : _a.destroy();
                        this.registerListener();
                    }
                });
            }
            window.addEventListener('OnDestroy', this.destroy.bind(this));
        }
        /**
         * Registers this component's Bing map listener.
         */
        registerListener() {
            var _a;
            if (((_a = this.props.delay) !== null && _a !== void 0 ? _a : 0) > 0) {
                setTimeout(() => {
                    if (this.isDestroyed) {
                        return;
                    }
                    this.mapListener = RegisterViewListener('JS_LISTENER_MAPS', this.onListenerRegistered.bind(this));
                }, this.props.delay);
            }
            else {
                this.mapListener = RegisterViewListener('JS_LISTENER_MAPS', this.onListenerRegistered.bind(this));
            }
        }
        /**
         * A callback called when this component's Bing map listener is registered.
         */
        onListenerRegistered() {
            if (this.isDestroyed || this.isListenerRegistered) {
                return;
            }
            this.mapListener.on('MapBinded', this.onMapBound);
            this.mapListener.on('MapUpdated', this.onMapUpdate);
            this.isListenerRegistered = true;
            this.mapListener.trigger('JS_BIND_BINGMAP', this.props.id, this.bingFlags);
        }
        /**
         * Gets Bing flags for this component's Bing mode and a Bing reference mode.
         * @param reference A Bing reference mode.
         * @returns Bing flags for this component's Bing mode and the specified Bing reference mode.
         */
        getBingFlags(reference) {
            let flags = 0;
            if (this.props.mode === EBingMode.HORIZON) {
                flags |= BingMapsFlags.FL_BINGMAP_3D;
            }
            else if (this.props.mode === EBingMode.TOPVIEW) {
                flags |= BingMapsFlags.FL_BINGMAP_3D | BingMapsFlags.FL_BINGMAP_3D_TOPVIEW;
            }
            if (reference === EBingReference.PLANE) {
                flags |= BingMapsFlags.FL_BINGMAP_REF_PLANE;
            }
            else if (reference === EBingReference.AERIAL) {
                flags |= BingMapsFlags.FL_BINGMAP_REF_AERIAL;
            }
            return flags;
        }
        /**
         * Wakes this Bing component. Upon awakening, this component will synchronize its state to the Bing instance to which
         * it is bound.
         */
        wake() {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            this._isAwake = true;
            if (!this._isBound) {
                return;
            }
            this.setCurrentMapParams();
            // Only when not SVT, periodically send map params to Coherent in case another BingComponent binds to the same
            // bing instance and sends in the initial params set and overrides our params.
            if (!BitFlags.isAll(this.bingFlags, BingMapsFlags.FL_BINGMAP_3D)) {
                this.setCurrentMapParamsTimer = setInterval(this.setCurrentMapParams, 200);
            }
            (_a = this.earthColorsSub) === null || _a === void 0 ? void 0 : _a.resume(true);
            (_b = this.earthColorsElevationRangeSub) === null || _b === void 0 ? void 0 : _b.resume(true);
            (_c = this.skyColorSub) === null || _c === void 0 ? void 0 : _c.resume(true);
            (_d = this.referenceSub) === null || _d === void 0 ? void 0 : _d.resume(true);
            (_e = this.wxrModeSub) === null || _e === void 0 ? void 0 : _e.resume(true);
            (_f = this.wxrColorsSub) === null || _f === void 0 ? void 0 : _f.resume(true);
            (_g = this.resolutionSub) === null || _g === void 0 ? void 0 : _g.resume(true);
            (_h = this.isoLinesSub) === null || _h === void 0 ? void 0 : _h.resume(true);
        }
        /**
         * Puts this Bing component to sleep. While asleep, this component cannot make changes to the Bing instance to which
         * it is bound.
         */
        sleep() {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            this._isAwake = false;
            if (!this._isBound) {
                return;
            }
            if (this.setCurrentMapParamsTimer !== null) {
                clearInterval(this.setCurrentMapParamsTimer);
            }
            (_a = this.earthColorsSub) === null || _a === void 0 ? void 0 : _a.pause();
            (_b = this.earthColorsElevationRangeSub) === null || _b === void 0 ? void 0 : _b.pause();
            (_c = this.skyColorSub) === null || _c === void 0 ? void 0 : _c.pause();
            (_d = this.referenceSub) === null || _d === void 0 ? void 0 : _d.pause();
            (_e = this.wxrModeSub) === null || _e === void 0 ? void 0 : _e.pause();
            (_f = this.wxrColorsSub) === null || _f === void 0 ? void 0 : _f.pause();
            (_g = this.resolutionSub) === null || _g === void 0 ? void 0 : _g.pause();
            (_h = this.isoLinesSub) === null || _h === void 0 ? void 0 : _h.pause();
        }
        /**
         * Sets the center position and radius.
         * @param pos The center position.
         * @param radius The radius, in meters.
         */
        setPositionRadius(pos, radius) {
            this.pos = pos;
            this.radius = Math.max(radius, 10); // Not sure if bad things happen when radius is 0, so we just clamp it to 10 meters.
            if (this._isBound && this._isAwake) {
                if (this.positionRadiusInhibitFramesRemaining > 0) {
                    this.isPositionRadiusPending = true;
                }
                else {
                    Coherent.call('SET_MAP_PARAMS', this.uid, this.pos, this.radius);
                }
            }
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("img", { ref: this.imgRef, src: '', style: 'position: absolute; left: 0; top: 0; width: 100%; height: 100%;', class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }));
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
            this.isDestroyed = true;
            this._isBound = false;
            if (this.setCurrentMapParamsTimer !== null) {
                clearInterval(this.setCurrentMapParamsTimer);
            }
            (_a = this.gameStateSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.earthColorsSub) === null || _b === void 0 ? void 0 : _b.destroy();
            (_c = this.earthColorsElevationRangeSub) === null || _c === void 0 ? void 0 : _c.destroy();
            (_d = this.skyColorSub) === null || _d === void 0 ? void 0 : _d.destroy();
            (_e = this.referenceSub) === null || _e === void 0 ? void 0 : _e.destroy();
            (_f = this.wxrModeSub) === null || _f === void 0 ? void 0 : _f.destroy();
            (_g = this.wxrColorsSub) === null || _g === void 0 ? void 0 : _g.destroy();
            (_h = this.resolutionSub) === null || _h === void 0 ? void 0 : _h.destroy();
            (_j = this.isoLinesSub) === null || _j === void 0 ? void 0 : _j.destroy();
            (_k = this.mapListener) === null || _k === void 0 ? void 0 : _k.off('MapBinded', this.onMapBound);
            (_l = this.mapListener) === null || _l === void 0 ? void 0 : _l.off('MapUpdated', this.onMapUpdate);
            if (!this.props.skipUnbindOnDestroy) {
                (_m = this.mapListener) === null || _m === void 0 ? void 0 : _m.trigger('JS_UNBIND_BINGMAP', this.props.id);
            }
            this.isListenerRegistered = false;
            this.imgRef.instance.src = '';
            (_o = this.imgRef.instance.parentNode) === null || _o === void 0 ? void 0 : _o.removeChild(this.imgRef.instance);
            super.destroy();
        }
        /**
         * Resets the img element's src attribute.
         */
        resetImgSrc() {
            const imgRef = this.imgRef.getOrDefault();
            if (imgRef !== null) {
                const currentSrc = imgRef.src;
                imgRef.src = '';
                imgRef.src = currentSrc;
            }
        }
        /**
         * Converts an HTML hex color string to a numerical RGB value, as `R + G * 256 + B * 256^2`.
         * @param hexColor The hex color string to convert.
         * @returns The numerical RGB value equivalent of the specified hex color string, as `R + G * 256 + B * 256^2`.
         */
        static hexaToRGBColor(hexColor) {
            const hexStringColor = hexColor;
            let offset = 0;
            if (hexStringColor[0] === '#') {
                offset = 1;
            }
            const r = parseInt(hexStringColor.substr(0 + offset, 2), 16);
            const g = parseInt(hexStringColor.substr(2 + offset, 2), 16);
            const b = parseInt(hexStringColor.substr(4 + offset, 2), 16);
            return BingComponent.rgbColor(r, g, b);
        }
        /**
         * Converts a numerical RGB value to an HTML hex color string.
         * @param rgb The numerical RGB value to convert, as `R + G * 256 + B * 256^2`.
         * @param poundPrefix Whether to include the pound (`#`) prefix in the converted string. Defaults to `true`.
         * @returns The HTML hex color string equivalent of the specified numerical RGB value.
         */
        static rgbToHexaColor(rgb, poundPrefix = true) {
            const b = Math.floor((rgb % (256 * 256 * 256)) / (256 * 256));
            const g = Math.floor((rgb % (256 * 256)) / 256);
            const r = rgb % 256;
            return `${poundPrefix ? '#' : ''}${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
        }
        /**
         * Converts RGB color components to a numerical RGB value, as `R + G * 256 + B * 256^2`.
         * @param r The red component, from 0 to 255.
         * @param g The green component, from 0 to 255.
         * @param b The blue component, from 0 to 255.
         * @returns The numerical RGB value of the specified components, as `R + G * 256 + B * 256^2`.
         */
        static rgbColor(r, g, b) {
            return 256 * 256 * b + 256 * g + r;
        }
        /**
         * Converts an HTML hex color string to a numerical RGBA value, as `R + G * 256 + B * 256^2 + A * 256^3`.
         * @param hexColor The hex color string to convert.
         * @returns The numerical RGBA value equivalent of the specified hex color string, as
         * `R + G * 256 + B * 256^2 + A * 256^3`.
         */
        static hexaToRGBAColor(hexColor) {
            const hexStringColor = hexColor;
            let offset = 0;
            if (hexStringColor[0] === '#') {
                offset = 1;
            }
            const r = parseInt(hexStringColor.substr(0 + offset, 2), 16);
            const g = parseInt(hexStringColor.substr(2 + offset, 2), 16);
            const b = parseInt(hexStringColor.substr(4 + offset, 2), 16);
            const a = parseInt(hexStringColor.substr(6 + offset, 2), 16);
            return BingComponent.rgbaColor(r, g, b, a);
        }
        /**
         * Converts a numerical RGBA value to an HTML hex color string.
         * @param rgba The numerical RGBA value to convert, as `R + G * 256 + B * 256^2 + A * 256^3`.
         * @param poundPrefix Whether to include the pound (`#`) prefix in the converted string. Defaults to `true`.
         * @returns The HTML hex color string equivalent of the specified numerical RGBA value.
         */
        static rgbaToHexaColor(rgba, poundPrefix = true) {
            const a = Math.floor((rgba % (256 * 256 * 256 * 256)) / (256 * 256 * 256));
            const b = Math.floor((rgba % (256 * 256 * 256)) / (256 * 256));
            const g = Math.floor((rgba % (256 * 256)) / 256);
            const r = rgba % 256;
            return `${poundPrefix ? '#' : ''}${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${a.toString(16).padStart(2, '0')}`;
        }
        /**
         * Converts RGBA color components to a numerical RGBA value, as `R + G * 256 + B * 256^2 + A * 256^3`.
         * @param r The red component, from 0 to 255.
         * @param g The green component, from 0 to 255.
         * @param b The blue component, from 0 to 255.
         * @param a The alpha component, from 0 to 255.
         * @returns The numerical RGBA value of the specified components, as `R + G * 256 + B * 256^2 + A * 256^3`.
         */
        static rgbaColor(r, g, b, a) {
            return 256 * 256 * 256 * a + 256 * 256 * b + 256 * g + r;
        }
        /**
         * Creates a full Bing component earth colors array. The earth colors array will contain the specified water color
         * and terrain colors (including interpolated values between the explicitly defined ones, as necessary).
         * @param waterColor The desired water color, as a hex string with the format `#hhhhhh`.
         * @param terrainColors An array of desired terrain colors at specific elevations. Elevations should be specified in
         * feet and colors as hex strings with the format `#hhhhhh`.
         * @param minElevation The minimum elevation to which to assign a color, in feet. Defaults to 0.
         * @param maxElevation The maximum elevation to which to assign a color, in feet. Defaults to 30000.
         * @param stepCount The number of terrain color steps. Defaults to 61.
         * @returns a full Bing component earth colors array.
         */
        // eslint-disable-next-line jsdoc/require-jsdoc
        static createEarthColorsArray(waterColor, terrainColors, minElevation = 0, maxElevation = 30000, stepCount = 61) {
            const earthColors = [BingComponent.hexaToRGBColor(waterColor)];
            const curve = new Avionics.Curve();
            curve.interpolationFunction = Avionics.CurveTool.StringColorRGBInterpolation;
            for (let i = 0; i < terrainColors.length; i++) {
                curve.add(terrainColors[i].elev, terrainColors[i].color);
            }
            const elevationStep = (maxElevation - minElevation) / Math.max(stepCount - 1, 1);
            for (let i = 0; i < stepCount; i++) {
                const color = curve.evaluate(minElevation + i * elevationStep);
                earthColors[i + 1] = BingComponent.hexaToRGBColor(color);
            }
            return earthColors;
        }
    }
    /** The default resolution of the Bing Map along both horizontal and vertical axes, in pixels. */
    BingComponent.DEFAULT_RESOLUTION = 1024;
    BingComponent.DEFAULT_WEATHER_COLORS = [
        [BingComponent.hexaToRGBAColor('#00000000'), 0.5],
        [BingComponent.hexaToRGBAColor('#004d00ff'), 2.75],
        [BingComponent.hexaToRGBAColor('#cb7300ff'), 12.5],
        [BingComponent.hexaToRGBAColor('#ff0000ff'), 12.5]
    ];
    BingComponent.POSITION_RADIUS_INHIBIT_FRAMES = 10;

    /**
     * A component which displays a number with units.
     */
    class AbstractNumberUnitDisplay extends DisplayComponent {
        constructor() {
            super(...arguments);
            /** A subscribable which provides the value to display. */
            this.value = ('isSubscribable' in this.props.value)
                ? this.props.value
                : Subject.create(this.props.value);
            /** A subscribable which provides the unit type in which to display the value. */
            this.displayUnit = this.props.displayUnit !== null && ('isSubscribable' in this.props.displayUnit)
                ? this.props.displayUnit
                : Subject.create(this.props.displayUnit);
        }
        /** @inheritdoc */
        onAfterRender() {
            this.valueSub = this.value.sub(this.onValueChanged.bind(this), true);
            this.displayUnitSub = this.displayUnit.sub(this.onDisplayUnitChanged.bind(this), true);
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            (_a = this.valueSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.displayUnitSub) === null || _b === void 0 ? void 0 : _b.destroy();
        }
    }

    /**
     * A scrolling digit display. The display supports number bases greater than or equal to 3. The display renders a
     * one digit for each of the following values:
     * ```
     * -(base + 2), -(base + 1), -(base), ... , -1, 0, 1, ... , base, base + 1, base + 2`
     * ```
     * The total number of rendered digits equals `(base + 2) * 2 + 1`. The display will scroll between the rendered
     * digits based on a bound value.
     *
     * When styling the scroller with CSS, select the `digit-scroller-digit` class to style all rendered digits. Each
     * individual digit can also be selected with the `digit-scroller-digit-[index]` classes, where `[index]` is replaced
     * with `0, 1, 2, ...`, starting with the lowest-valued digit. Select the `digit-scroller-nan` class to style the text
     * rendered for `NaN` values. The `--digit-scroller-line-height` variable is used to control the vertical spacing
     * between each digit (defaults to `1em`). The `--digit-scroller-line-offset-y` variable is used to control the
     * vertical offset of each digit (defaults to `0px`).
     */
    class DigitScroller extends DisplayComponent {
        /** @inheritdoc */
        constructor(props) {
            var _a;
            super(props);
            this.digitCount = (this.props.base + 2) * 2 + 1;
            this.translationPerDigit = 100 / this.digitCount;
            this.tapeStyle = ObjectSubject.create({
                display: '',
                position: 'absolute',
                left: '0',
                top: `calc(50% - var(--digit-scroller-line-height, 1em) * ${this.digitCount / 2})`,
                width: '100%',
                height: `calc(var(--digit-scroller-line-height, 1em) * ${this.digitCount})`,
                transform: 'translate3d(0, 0, 0)'
            });
            this.nanTextStyle = ObjectSubject.create({
                display: 'none',
                position: 'absolute',
                left: '0%',
                top: '50%',
                width: '100%',
                transform: 'translateY(-50%)'
            });
            this.digitPlaceFactor = this.props.factor;
            this.scrollThreshold = (_a = this.props.scrollThreshold) !== null && _a !== void 0 ? _a : 0;
            this.translateY = Subject.create(0);
            if (props.base < 3 || Math.floor(props.base) !== props.base) {
                throw new Error(`DigitScroller: invalid number base (${this.props.base})`);
            }
            if (props.factor === 0) {
                throw new Error(`DigitScroller: invalid factor (${props.factor})`);
            }
        }
        /** @inheritdoc */
        onAfterRender() {
            this.translateY.sub(translateY => {
                this.tapeStyle.set('transform', `translate3d(0, ${translateY}%, 0)`);
            });
            this.valueSub = this.props.value.sub(this.update.bind(this), true);
        }
        /**
         * Updates this display.
         * @param value This display's value.
         */
        update(value) {
            if (isNaN(value)) {
                this.nanTextStyle.set('display', '');
                this.tapeStyle.set('display', 'none');
                return;
            }
            this.nanTextStyle.set('display', 'none');
            this.tapeStyle.set('display', '');
            const base = this.props.base;
            const valueSign = value < 0 ? -1 : 1;
            const valueAbs = Math.abs(value);
            let pivot = Math.floor(valueAbs / this.digitPlaceFactor) * this.digitPlaceFactor;
            let digit = Math.floor(pivot / this.digitPlaceFactor) % base;
            let digitTranslate = (valueAbs - pivot) / this.digitPlaceFactor;
            const threshold = this.scrollThreshold / this.digitPlaceFactor;
            digitTranslate = (digitTranslate > threshold) ? (digitTranslate - threshold) / (1 - threshold) : 0;
            if (digitTranslate >= 0.5) {
                pivot += this.digitPlaceFactor;
                digit = (digit + 1) % base;
                digitTranslate -= 1;
            }
            let tapeTranslate = 0;
            if (pivot <= this.digitPlaceFactor) {
                tapeTranslate = (digit + digitTranslate) * valueSign * this.translationPerDigit;
            }
            else {
                tapeTranslate = (((digit + base - 2) % base + 2) + digitTranslate) * valueSign * this.translationPerDigit;
            }
            this.translateY.set(MathUtils.round(tapeTranslate, 0.1));
        }
        /** @inheritdoc */
        render() {
            var _a, _b;
            let cssClass;
            if (this.props.class !== undefined && typeof this.props.class === 'object') {
                cssClass = SetSubject.create(['digit-scroller']);
                this.cssClassSub = exports.FSComponent.bindCssClassSet(cssClass, this.props.class, ['digit-scroller']);
            }
            else {
                cssClass = `digit-scroller ${(_a = this.props.class) !== null && _a !== void 0 ? _a : ''}`;
            }
            return (exports.FSComponent.buildComponent("div", { class: cssClass, style: 'overflow: hidden' },
                exports.FSComponent.buildComponent("div", { class: 'digit-scroller-digit-tape', style: this.tapeStyle }, this.renderDigits()),
                exports.FSComponent.buildComponent("div", { class: 'digit-scroller-nan', style: this.nanTextStyle }, (_b = this.props.nanString) !== null && _b !== void 0 ? _b : '–')));
        }
        /**
         * Renders text for each of this display's individual digits.
         * @returns This display's individual digit text, as an array of VNodes.
         */
        renderDigits() {
            var _a;
            const base = this.props.base;
            const renderFunc = (_a = this.props.renderDigit) !== null && _a !== void 0 ? _a : ((digit) => (Math.abs(digit) % base).toString());
            // Digits to render: -(base + 2), -(base + 1), -(base), -(base - 1), ... -1, 0, 1, ... , base - 1, base, base + 1, base + 2
            const zeroIndexOffset = base + 2;
            return Array.from({ length: this.digitCount }, (v, index) => {
                const digit = zeroIndexOffset - index;
                return (exports.FSComponent.buildComponent("div", { style: `position: absolute; left: 0; top: calc(var(--digit-scroller-line-offset-y, 0px) + ${50 + (index - zeroIndexOffset - 0.5) * this.translationPerDigit}%); width: 100%; height: ${this.translationPerDigit}%; line-height: var(--digit-scroller-line-height, 1em);` },
                    exports.FSComponent.buildComponent("span", { class: `digit-scroller-digit digit-scroller-digit-${index}`, style: 'vertical-align: baseline;' }, renderFunc(digit))));
            });
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            super.destroy();
            (_a = this.valueSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.cssClassSub) === null || _b === void 0 ? void 0 : _b.destroy();
        }
    }

    exports.DurationDisplayFormat = void 0;
    (function (DurationDisplayFormat) {
        /** hh:mm:ss. */
        DurationDisplayFormat[DurationDisplayFormat["hh_mm_ss"] = 0] = "hh_mm_ss";
        /** hh:mm. */
        DurationDisplayFormat[DurationDisplayFormat["hh_mm"] = 1] = "hh_mm";
        /** mm:ss. */
        DurationDisplayFormat[DurationDisplayFormat["mm_ss"] = 2] = "mm_ss";
        /** hh:mm if value is greater or equal to 1 hour, otherwise mm:ss. */
        DurationDisplayFormat[DurationDisplayFormat["hh_mm_or_mm_ss"] = 3] = "hh_mm_or_mm_ss";
    })(exports.DurationDisplayFormat || (exports.DurationDisplayFormat = {}));
    exports.DurationDisplayDelim = void 0;
    (function (DurationDisplayDelim) {
        /** Colon (`:`). */
        DurationDisplayDelim[DurationDisplayDelim["Colon"] = 0] = "Colon";
        /** `:` if hh:mm:ss or mm:ss, `+` if hh:mm. */
        DurationDisplayDelim[DurationDisplayDelim["ColonOrCross"] = 1] = "ColonOrCross";
        /** Space (` `). */
        DurationDisplayDelim[DurationDisplayDelim["Space"] = 2] = "Space";
    })(exports.DurationDisplayDelim || (exports.DurationDisplayDelim = {}));
    /**
     * A component which displays duration values.
     */
    class DurationDisplay extends DisplayComponent {
        /** @inheritdoc */
        constructor(props) {
            super(props);
            this.value = ('isSubscribable' in this.props.value)
                ? this.props.value
                : Subject.create(this.props.value);
            this.options = Object.assign({}, DurationDisplay.DEFAULT_OPTIONS, this.props.options);
            this.negativeSign = this.options.useMinusSign ? '−' : '-';
            this.positiveSign = this.options.forceSign ? '+' : '';
            this.text = Subject.create('');
            switch (this.options.delim) {
                case exports.DurationDisplayDelim.Colon:
                    this.delim = ':';
                    break;
                case exports.DurationDisplayDelim.Space:
                    this.delim = ' ';
                    break;
                default:
                    this.delim = '';
            }
        }
        /** @inheritdoc */
        onAfterRender() {
            this.valueSub = this.value.sub(this.onValueChanged.bind(this), true);
        }
        /**
         * A callback which is called when this component's bound value changes.
         * @param value The new value.
         */
        onValueChanged(value) {
            this.setDisplay(value);
        }
        /**
         * Displays this component's current value.
         * @param value The current value.
         */
        setDisplay(value) {
            let text;
            if (value.isNaN()) {
                text = this.options.nanString;
            }
            else {
                let hrText = '';
                let minText = '';
                let secText = '';
                let hrUnitText = '';
                let minUnitText = '';
                let secUnitText = '';
                let hrDelim = '';
                let minDelim = '';
                const valueAsSeconds = Math.abs(value.asUnit(UnitType.SECOND));
                const isNegative = value.number < 0;
                let hours = Math.floor(valueAsSeconds / 3600);
                if (this.options.format != exports.DurationDisplayFormat.mm_ss && !(this.options.format === exports.DurationDisplayFormat.hh_mm_or_mm_ss && hours == 0)) {
                    hrText = hours.toFixed(0);
                    if (this.options.delim === exports.DurationDisplayDelim.ColonOrCross) {
                        if (this.options.format === exports.DurationDisplayFormat.hh_mm_or_mm_ss || this.options.format === exports.DurationDisplayFormat.hh_mm) {
                            hrDelim = '+';
                        }
                        else {
                            hrDelim = ':';
                        }
                    }
                    else {
                        hrDelim = this.delim;
                    }
                }
                const hoursInMinutes = hours * 60;
                let minutes;
                let seconds;
                if (this.options.format === exports.DurationDisplayFormat.hh_mm || (this.options.format === exports.DurationDisplayFormat.hh_mm_or_mm_ss && hours !== 0)) {
                    minutes = valueAsSeconds / 60 - hoursInMinutes;
                    minText = this.options.numberFormatter(minutes);
                }
                else {
                    minutes = Math.floor(valueAsSeconds / 60 - hoursInMinutes);
                    minText = minutes.toFixed(0);
                    minDelim = this.options.delim === exports.DurationDisplayDelim.ColonOrCross ? ':' : this.delim;
                    seconds = valueAsSeconds - (hoursInMinutes + minutes) * 60;
                    secText = this.options.numberFormatter(seconds);
                }
                if (secText && secText.replace(/\b0+/, '').substring(0, 2) === '60') {
                    seconds = parseFloat(secText) - 60;
                    minutes++;
                    secText = this.options.numberFormatter(seconds);
                    minText = `${minutes}`;
                }
                if (minText && minText.replace(/\b0+/, '').substring(0, 2) === '60' && hrText) {
                    if (secText) {
                        minutes = 0;
                        minText = '00';
                    }
                    else {
                        minutes = parseFloat(minText) - 60;
                        minText = this.options.numberFormatter(minutes);
                    }
                    hours++;
                    hrText = `${hours}`;
                }
                // pad parts with leading zeroes
                if (hrText) {
                    hrText = hrText.padStart(this.options.pad, '0');
                    if (secText) {
                        minText = minText.padStart(2, '0');
                        secText = DurationDisplay.padIntegerPart(secText.replace(/^0+/, ''), 2, '0');
                    }
                    else {
                        minText = DurationDisplay.padIntegerPart(minText.replace(/^0+/, ''), 2, '0');
                    }
                }
                else {
                    minText = minText.padStart(this.options.pad, '0');
                    secText = DurationDisplay.padIntegerPart(secText.replace(/^0+/, ''), 2, '0');
                }
                // format units
                if (this.options.showUnits) {
                    hrText && (hrUnitText = this.options.unitFormatter(parseFloat(hrText), UnitType.HOUR));
                    minUnitText = this.options.unitFormatter(parseFloat(minText), UnitType.MINUTE);
                    secText && (secUnitText = this.options.unitFormatter(parseFloat(secText), UnitType.SECOND));
                }
                // compute sign
                const sign = isNegative ? this.negativeSign : this.positiveSign;
                text = `${sign}${hrText}${hrUnitText}${hrDelim}${minText}${minUnitText}${minDelim}${secText}${secUnitText}`;
            }
            this.text.set(text);
        }
        /**
         * Pads the integer part of a string which represents a number.
         * @param str A string which represents a number.
         * @param maxLength The length to which the integer part of the string will be padded.
         * @param fillString The string with which to pad the original string.
         * @returns a new string which is the result of padding the original string.
         */
        static padIntegerPart(str, maxLength, fillString) {
            const decimalIndex = str.indexOf('.');
            return str.padStart(decimalIndex < 0 ? maxLength : str.length - decimalIndex + maxLength, fillString);
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '', style: 'white-space: nowrap;' }, this.text));
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.valueSub) === null || _a === void 0 ? void 0 : _a.destroy();
        }
    }
    /** Default formatting options. */
    DurationDisplay.DEFAULT_OPTIONS = {
        pad: 0,
        format: exports.DurationDisplayFormat.hh_mm_ss,
        delim: exports.DurationDisplayDelim.Colon,
        showUnits: false,
        numberFormatter: (value) => value.toFixed(0),
        unitFormatter: (value, unit) => unit.name[0],
        useMinusSign: false,
        forceSign: false,
        nanString: ''
    };

    var DmsDirection;
    (function (DmsDirection) {
        DmsDirection["NORTH"] = "N";
        DmsDirection["SOUTH"] = "S";
        DmsDirection["WEST"] = "W";
        DmsDirection["EAST"] = "E";
    })(DmsDirection || (DmsDirection = {}));
    /**
     * A class to format latitude/longitude to DMS.
     * @class DmsFormatter
     */
    class DmsFormatter {
        constructor() {
            this.coordsParts = {
                direction: DmsDirection.NORTH,
                degrees: 0,
                minutes: 0,
                seconds: 0
            };
        }
        /**
         * Builds a DMS string out of the given latitude.
         * @param value The latitude.
         * @param spaceAfterDirection Whether to insert a space after the direction letter.
         * @param minuteApostrophe Whether to insert an apostrophe (') after the string.
         * @param precision Decimal precision for the minutes part.
         * @param padMinutesTo Padding applied to integer part of minutes.
         * @returns The DMS string.
         */
        getLatDmsStr(value, spaceAfterDirection = true, minuteApostrophe = true, precision = 2, padMinutesTo = 3) {
            const parts = this.parseLat(value);
            if (parts.minutes >= 59.5) {
                parts.minutes = 0;
                parts.degrees++;
            }
            return `${parts.direction}${spaceAfterDirection ? ' ' : ''}${parts.degrees.toString().padStart(2, '0')}°${parts.minutes.toFixed(precision).padStart(padMinutesTo + precision, '0')}${minuteApostrophe ? '\'' : ''}`;
        }
        /**
         * Builds a DMS string out of the given longitude.
         * @param value The longitude.
         * @param minuteApostrophe Whether to insert an apostrophe (') after the string.
         * @param precision Decimal precision for the minutes part.
         * @param padMinutesTo Padding applied to integer part of minutes.
         * @returns The DMS string.
         */
        getLonDmsStr(value, minuteApostrophe = true, precision = 2, padMinutesTo = 3) {
            const parts = this.parseLon(value);
            if (parts.minutes >= 59.5) {
                parts.minutes = 0;
                parts.degrees++;
            }
            return `${parts.direction}${parts.degrees.toString().padStart(3, '0')}°${parts.minutes.toFixed(precision).padStart(padMinutesTo + precision, '0')}${minuteApostrophe ? '\'' : ''}`;
        }
        /**
         * Parses a latitude in to the dms parts.
         * @param value The latitude in degrees.
         * @returns The DMS parts.
         */
        parseLat(value) {
            this.coordsParts.direction = value < 0 ? DmsDirection.SOUTH : DmsDirection.NORTH;
            return this.parse(value);
        }
        /**
         * Parses a longitude in to the dms parts.
         * @param value The longitude in degrees.
         * @returns The DMS parts.
         */
        parseLon(value) {
            this.coordsParts.direction = value < 0 ? DmsDirection.WEST : DmsDirection.EAST;
            return this.parse(value);
        }
        /**
         * Parses the latitude/longitude.
         * @private
         * @param value The value to parse.
         * @returns The DMS parts.
         */
        parse(value) {
            value = Math.abs(value);
            this.coordsParts.degrees = Math.trunc(value);
            value = (value - this.coordsParts.degrees) * 60;
            this.coordsParts.minutes = value;
            this.coordsParts.seconds = (value - this.coordsParts.minutes) * 60;
            return this.coordsParts;
        }
    }

    /**
     * A component which displays lat/lon coordinates.
     */
    class LatLonDisplay extends DisplayComponent {
        constructor() {
            super(...arguments);
            this.latPrefix = Subject.create('');
            this.latNum = Subject.create('');
            this.lonPrefix = Subject.create('');
            this.lonNum = Subject.create('');
            this.formatter = new DmsFormatter();
        }
        /** @inheritdoc */
        onAfterRender() {
            this.locationSub = this.props.location.sub(this.onLocationChanged.bind(this), true);
        }
        /**
         * A callback which is called when this component's bound location changes.
         * @param location The new location.
         */
        onLocationChanged(location) {
            if (isNaN(location.lat) || isNaN(location.lon)) {
                this.clearDisplay();
            }
            else {
                this.setDisplay(location);
            }
        }
        /**
         * Displays the formatted lat/lon coordinates of a location.
         * @param location A location.
         */
        setDisplay(location) {
            this.setCoordSub(this.latPrefix, this.latNum, this.formatter.parseLat(location.lat), 2);
            this.setCoordSub(this.lonPrefix, this.lonNum, this.formatter.parseLon(location.lon), 3);
        }
        /**
         * Sets coordinate subjects for a given set of coordinate values.
         * @param prefixSub The coordinate prefix subject.
         * @param numSub The coordinate number subject.
         * @param coordValues The DMS values of the coordinate.
         * @param padDeg The number of digits to which to pad the degrees value.
         */
        setCoordSub(prefixSub, numSub, coordValues, padDeg) {
            var _a;
            const prefix = coordValues.direction;
            let deg = coordValues.degrees;
            let minutes = Math.round((coordValues.minutes + coordValues.seconds / 60) * 100) / 100;
            if (minutes === 60) {
                // need to increment up degrees if minutes was rounded up to 60 from 59.xx.
                deg++;
                minutes = 0;
            }
            const fractionDigits = (_a = this.props.minFracDigits) !== null && _a !== void 0 ? _a : 2;
            prefixSub.set(prefix);
            numSub.set(`${deg.toString().padStart(padDeg, '0')}°${minutes.toFixed(fractionDigits).padStart(fractionDigits + 3, '0')}'`);
        }
        /**
         * Displays the blank default value.
         */
        clearDisplay() {
            var _a;
            const fractionDigits = (_a = this.props.minFracDigits) !== null && _a !== void 0 ? _a : 2;
            this.latPrefix.set('_');
            this.latNum.set(`__°__.${'_'.repeat(fractionDigits)}'`);
            this.lonPrefix.set('_');
            this.lonNum.set(`___°__.${'_'.repeat(fractionDigits)}'`);
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' },
                exports.FSComponent.buildComponent("div", { class: 'latlon-coord latlon-lat' },
                    exports.FSComponent.buildComponent("div", { class: 'latlon-prefix' }, this.latPrefix),
                    exports.FSComponent.buildComponent("div", { class: 'latlon-num', style: 'white-space: nowrap;' }, this.latNum)),
                exports.FSComponent.buildComponent("div", { class: 'latlon-coord latlon-lon' },
                    exports.FSComponent.buildComponent("div", { class: 'latlon-prefix' }, this.lonPrefix),
                    exports.FSComponent.buildComponent("div", { class: 'latlon-num', style: 'white-space: nowrap;' }, this.lonNum))));
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.locationSub) === null || _a === void 0 ? void 0 : _a.destroy();
        }
    }

    /**
     * The item position to focus a component's children when performing a focus operation.
     */
    exports.FocusPosition = void 0;
    (function (FocusPosition) {
        /** The component's most recently focused descendants will be focused. */
        FocusPosition["MostRecent"] = "MostRecent";
        /** The first focus-able child at each node in the descendant tree will be focused. */
        FocusPosition["First"] = "First";
        /** The last focus-able child at each node in the descendant tree will be focused. */
        FocusPosition["Last"] = "Last";
        /** No child components will be focused. */
        FocusPosition["None"] = "None";
    })(exports.FocusPosition || (exports.FocusPosition = {}));
    /**
     * A strategy to focus a component's children as part of a blur reconciliation operation.
     */
    exports.BlurReconciliation = void 0;
    (function (BlurReconciliation) {
        /** The component's first focus-able child will be focused. */
        BlurReconciliation["First"] = "First";
        /** The component's last focus-able child will be focused. */
        BlurReconciliation["Last"] = "Last";
        /**
         * The component's next focus-able child after the child that was blurred will be focused. If no such child exists,
         * then the last focus-able child before the child that was blurred will be focused.
         */
        BlurReconciliation["Next"] = "Next";
        /**
         * The component's last focus-able child before the child that was blurred will be focused. If no such child exists,
         * then the next focus-able child after the child that was blurred will be focused.
         */
        BlurReconciliation["Prev"] = "Prev";
        /** No child components will be focused. */
        BlurReconciliation["None"] = "None";
    })(exports.BlurReconciliation || (exports.BlurReconciliation = {}));
    /**
     * An abstract implementation of a component that forms the base of a Garmin-like UI control system. Subclasses should
     * implement an appropriate event handler interface (using the utility type `UiControlEventHandlers<Events>`) and have
     * their props implement the corresponding prop event handler interface (using the utility type
     * `UiControlPropEventHandlers<Events>`).
     * @template E An event definition type for events supported by this control.
     * @template P The component prop type for this control.
     */
    class HardwareUiControl extends DisplayComponent {
        /**
         * Creates an instance of a HardwareUiControl.
         * @param props The props for this component.
         */
        constructor(props) {
            super(props);
            this.focusedIndex = -1;
            this._isDisabled = false;
            this._isFocused = false;
            this._isIsolated = false;
            this._UICONTROL_ = true;
            this._isIsolated = this.props.isolateScroll !== undefined && this.props.isolateScroll;
        }
        /**
         * Gets the current number of registered child controls.
         * @returns The current number of registered child controls.
         */
        get length() {
            if (this.registeredControls !== undefined) {
                return this.registeredControls.length;
            }
            return 0;
        }
        /**
         * Gets whether or not the control is currently disabled.
         * @returns True if disabled, false otherwise.
         */
        get isDisabled() {
            return this._isDisabled;
        }
        /**
         * Gets whether or not the control is currently focused.
         * @returns True if disabled, false otherwise.
         */
        get isFocused() {
            return this._isFocused;
        }
        /**
         * Gets whether or not the control is currently in scroll isolation.
         * @returns True if currently in scroll isolation, false otherwise.
         */
        get isIsolated() {
            return this._isIsolated;
        }
        /**
         * An event called when the control receives focus.
         * @param source The control that emitted this event.
         */
        onFocused(source) {
            this.props.onFocused && this.props.onFocused(source);
        }
        /**
         * An event called when the control is blurred.
         * @param source The control that emitted this event.
         */
        onBlurred(source) {
            this.props.onBlurred && this.props.onBlurred(source);
        }
        /**
         * An event called when the control is enabled.
         * @param source The control that emitted this event.
         */
        onEnabled(source) {
            this.props.onEnabled && this.props.onEnabled(source);
        }
        /**
         * An event called when the control is disabled.
         * @param source The control that emitted this event.
         */
        onDisabled(source) {
            this.props.onDisabled && this.props.onDisabled(source);
        }
        /**
         * An event called when a control is registered with this control.
         * @param source The control that emitted this event.
         */
        onRegistered(source) {
            this.props.onRegistered && this.props.onRegistered(source);
        }
        /**
         * An event called when a control is unregistered from this control.
         * @param source The control that emitted this event.
         */
        onUnregistered(source) {
            this.props.onUnregistered && this.props.onUnregistered(source);
        }
        /**
         * Gets the focus position to apply when this control is focused from a scroll.
         * @param direction The direction of the scroll.
         * @returns The focus position to apply when this control is focused from a scroll.
         */
        getFocusPositionOnScroll(direction) {
            if (this.props.getFocusPositionOnScroll) {
                return this.props.getFocusPositionOnScroll(direction);
            }
            return direction === 'forward' ? exports.FocusPosition.First : exports.FocusPosition.Last;
        }
        /**
         * An event called when the control is scrolled.
         * @param direction The direction that is being requested to scroll.
         * @returns True if this control handled this event, false otherwise.
         */
        onScroll(direction) {
            if (this.registeredControls !== undefined && this.registeredControls.length > 0) {
                const delta = direction === 'forward' ? 1 : -1;
                for (let i = this.focusedIndex + delta; direction === 'forward' ? i < this.registeredControls.length : i >= 0; i += delta) {
                    const controlToFocus = this.registeredControls[i];
                    if (controlToFocus.focus(controlToFocus.getFocusPositionOnScroll(direction))) {
                        this.onAfterScroll(controlToFocus, i);
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * An event called when a scroll operation has completed.
         * @param control The control that was scrolled to.
         * @param index The index of the control in the collection of registered controls.
         */
        onAfterScroll(control, index) {
            this.props.onAfterScroll && this.props.onAfterScroll(control, index);
        }
        /**
         * Scrolls the currently focused control in the supplied direction.
         * @param direction The direction that is being requested to scroll.
         * @returns True if propagation should be stopped, false otherwise.
         */
        scroll(direction) {
            if (!this.isFocused) {
                return false;
            }
            const controlToScroll = this.getFocusedComponentPath()[0];
            if (controlToScroll !== undefined) {
                return controlToScroll.tryPerformScroll(direction);
            }
            else {
                return this.tryPerformScroll(direction);
            }
        }
        /**
         * Attempts to perform a scroll operation on the control, propagating the operation
         * upward in the tree if the control does not handle the operation.
         * @param direction The direction that is being requested to scroll.
         * @returns True if propagation should be stopped, false otherwise.
         */
        tryPerformScroll(direction) {
            let handled = false;
            if (this.props.onScroll !== undefined) {
                handled = this.props.onScroll(direction);
            }
            else {
                handled = this.onScroll(direction);
            }
            if (!handled) {
                const currentlyIsolated = this.isFocused && this.isIsolated;
                if (this.parent !== undefined && !currentlyIsolated) {
                    return this.parent.tryPerformScroll(direction);
                }
                else if (currentlyIsolated) {
                    return true;
                }
            }
            return handled;
        }
        /**
         * Triggers an event on this control. The event will first be routed to the deepest focused descendent of this
         * control and will propagate up the control tree until it is handled or there are no more controls to which to
         * propagate.
         * @param event The event to trigger.
         * @param source The source of the event. Defaults to this if not supplied.
         * @param args Additional arguments to pass to the event handler.
         * @returns True if the event was handled, false otherwise.
         */
        triggerEvent(event, source, ...args) {
            const canListen = this.isFocused || this.parent === undefined;
            if (!canListen) {
                return false;
            }
            const focusedControl = this.getFocusedComponentPath()[0];
            if (focusedControl !== undefined) {
                return focusedControl.propagateEvent(event, focusedControl, args);
            }
            return false;
        }
        /**
         * Propagates an event up the control tree.
         * @param event The event to propagate.
         * @param source The source of the event.
         * @param args Additional arguments to pass to the event handler.
         * @returns True if the event was handled, false otherwise.
         */
        propagateEvent(event, source, args) {
            const handler = this[`on${String(event)}`];
            const propHandler = this.props[`on${String(event)}`];
            // Class-defined handlers get priority over prop-defined handlers
            // Prop-defined handlers are not called if a class-defined handler exists -> this is to allow subclasses to
            // restrict which events get sent to prop-defined handlers if they choose.
            const stopPropagation = (!!handler && handler.call(this, source, ...args)) || (!!propHandler && propHandler(source, ...args));
            if (!stopPropagation && this.parent !== undefined) {
                return this.parent.propagateEvent(event, this.parent, args);
            }
            return stopPropagation;
        }
        /**
         * Validates that the control can be focused by checking if any ancestors in the
         * control tree are disabled.
         * @returns True if there are no disabled ancestors, false otherwise.
         */
        canBeFocused() {
            let canFocus = true;
            if (!this._isDisabled) {
                if (this.parent !== undefined) {
                    canFocus = this.parent.canBeFocused();
                }
            }
            else {
                canFocus = false;
            }
            return canFocus;
        }
        /**
         * Brings focus to the control. Focusing the control will also blur the currently
         * focused control, if any.
         * @param focusPosition The focus position to activate for descendents of this control.
         * @returns Whether this control was successfully focused.
         */
        focus(focusPosition) {
            if (!this.canBeFocused()) {
                return false;
            }
            const focusStack = this.buildFocusPath(focusPosition);
            if (focusStack.length === 0) {
                return false;
            }
            // Top of the stack is always 'this', and will be repeated by getFocusRootPath() if we don't remove it
            focusStack.pop();
            this.getDeepestFocusedAncestorPath(focusStack);
            const focusRoot = focusStack[focusStack.length - 1];
            const blurStack = focusRoot.getFocusedComponentPath();
            if (blurStack.length > 0) {
                // Top of the blur stack is the deepest common ancestor of the old focused leaf and this control.
                // This ancestor will be focused after this operation, so we need to remove it from the blur stack.
                blurStack.pop();
                for (let i = 0; i < blurStack.length; i++) {
                    blurStack[i]._isFocused = false;
                }
            }
            // Top of the focus stack is the deepest common ancestor of the old focused leaf and this control, OR the root of
            // the control tree if nothing in the tree is focused -> either way, the control will be focused after this
            // operation, so if the control is already focused, we need to remove it from the focus stack.
            if (focusRoot.isFocused) {
                focusStack.pop();
            }
            for (let i = 0; i < focusStack.length; i++) {
                const control = focusStack[i];
                const parent = control.parent;
                control._isFocused = true;
                if (parent !== undefined && parent.registeredControls !== undefined) {
                    parent.focusedIndex = parent.registeredControls.indexOf(control);
                }
            }
            while (blurStack.length > 0) {
                const control = blurStack.pop();
                if (control !== undefined) {
                    control.onBlurred(control);
                }
            }
            while (focusStack.length > 0) {
                const control = focusStack.pop();
                if (control !== undefined) {
                    control.onFocused(control);
                }
            }
            return true;
        }
        /**
         * Gets the path from this control to the deepest descendent control that is focused. If this control is not focused,
         * then the path is empty.
         * @param path The stack of control nodes defining the path to the currently
         * focused descendent control.
         * @returns A stack of nodes that defines the path to the deepest focused descendent
         * node, in order of deepest descendent first.
         */
        getFocusedComponentPath(path) {
            if (path === undefined) {
                path = [];
            }
            if (!this._isFocused) {
                return path;
            }
            // eslint-disable-next-line @typescript-eslint/no-this-alias
            let currentControl = this;
            while (currentControl !== undefined) {
                path.splice(0, 0, currentControl);
                if (currentControl.registeredControls !== undefined) {
                    currentControl = currentControl.registeredControls.find(c => c.isFocused);
                }
                else {
                    currentControl = undefined;
                }
            }
            return path;
        }
        /**
         * Gets the path from this control to its deepest ancestor that is focused (including itself). If none of this
         * control's ancestors are focused, the path will contain this control and all of its ancestors up to and including
         * the root of its control tree.
         * @param path An array in which to store the path.
         * @returns A stack of controls that defines the path from this control to its deepest focused ancestor, ordered
         * from descendents to ancestors (the control at the shallowest tree depth is located at the top of the stack).
         */
        getDeepestFocusedAncestorPath(path) {
            if (path === undefined) {
                path = [];
            }
            // eslint-disable-next-line @typescript-eslint/no-this-alias
            let currentControl = this;
            while (currentControl !== undefined) {
                path.push(currentControl);
                if (currentControl !== this && currentControl.isFocused) {
                    break;
                }
                currentControl = currentControl.parent;
            }
            return path;
        }
        /**
         * Builds the path of controls to focus from this control downward in children based on the provided default focus
         * position. If this control cannot be focused, the path will be empty.
         * @param focusPosition The focus position to use to build the path.
         * @param focusStack The stack in which to store the path.
         * @returns A stack of components that defines the path from the deepest descendent to focus to this control if this
         * control were to be focused with the specified focus position, ordered from descendents to ancestors (the control
         * at the shallowest tree depth is located at the top of the stack).
         */
        buildFocusPath(focusPosition, focusStack) {
            var _a;
            if (focusStack === undefined) {
                focusStack = [];
            }
            if (this._isDisabled) {
                return focusStack;
            }
            //focusStack.splice(0, 0, currentControl);
            const originalStackDepth = focusStack.length;
            const childControls = this.registeredControls;
            if (childControls !== undefined) {
                switch (focusPosition) {
                    case exports.FocusPosition.MostRecent:
                        // Attempt to focus the most recent focused child. If this fails, fall back to FocusPosition.First.
                        (_a = childControls[this.focusedIndex]) === null || _a === void 0 ? void 0 : _a.buildFocusPath(exports.FocusPosition.MostRecent, focusStack);
                        if (focusStack.length > originalStackDepth) {
                            break;
                        }
                    // eslint-disable-next-line no-fallthrough
                    case exports.FocusPosition.First:
                        for (let i = 0; i < childControls.length; i++) {
                            childControls[i].buildFocusPath(exports.FocusPosition.First, focusStack);
                            if (focusStack.length > originalStackDepth) {
                                break;
                            }
                        }
                        break;
                    case exports.FocusPosition.Last:
                        for (let i = childControls.length - 1; i >= 0; i--) {
                            childControls[i].buildFocusPath(exports.FocusPosition.Last, focusStack);
                            if (focusStack.length > originalStackDepth) {
                                break;
                            }
                        }
                        break;
                }
            }
            // If this control requires child focus, make sure a child was able to be focused before adding this control to the stack.
            if (!this.props.requireChildFocus || focusStack.length > originalStackDepth) {
                focusStack.push(this);
            }
            return focusStack;
        }
        /**
         * Blurs, or removes focus, from the component.
         */
        blur() {
            var _a, _b;
            if (!this._isFocused) {
                return;
            }
            let indexInParent = -1;
            if (this.parent) {
                indexInParent = (_b = (_a = this.parent.registeredControls) === null || _a === void 0 ? void 0 : _a.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
            }
            const blurStack = this.getFocusedComponentPath();
            for (let i = 0; i < blurStack.length; i++) {
                blurStack[i]._isFocused = false;
            }
            while (blurStack.length > 0) {
                const control = blurStack.pop();
                if (control !== undefined) {
                    control.onBlurred(control);
                }
            }
            if (this.parent && indexInParent >= 0) {
                this.parent.handleNoFocusedChild(indexInParent, this);
            }
        }
        /**
         * Handles the case where this control is left focused with no focused child control after a child control is
         * blurred.
         * @param indexBlurred The index of the child control that was blurred. If the child is no longer registered, then
         * this is the index of the child prior to being unregistered.
         * @param childBlurred The child control that was blurred.
         */
        handleNoFocusedChild(indexBlurred, childBlurred) {
            var _a;
            if (!this._isFocused || this.getFocusedIndex() >= 0) {
                return;
            }
            const reconciliation = this.reconcileChildBlur(indexBlurred, childBlurred);
            if (this.registeredControls) {
                if (typeof reconciliation === 'number') {
                    const controlToFocus = (_a = this.registeredControls) === null || _a === void 0 ? void 0 : _a[reconciliation];
                    controlToFocus === null || controlToFocus === void 0 ? void 0 : controlToFocus.focus(exports.FocusPosition.First);
                }
                else {
                    switch (reconciliation) {
                        case exports.BlurReconciliation.First:
                            this.focus(exports.FocusPosition.First);
                            break;
                        case exports.BlurReconciliation.Last:
                            this.focus(exports.FocusPosition.Last);
                            break;
                        case exports.BlurReconciliation.Next:
                            for (let i = Math.max(indexBlurred + (this.registeredControls[indexBlurred] === childBlurred ? 1 : 0), 0); i < this.registeredControls.length; i++) {
                                if (this.registeredControls[i].focus(exports.FocusPosition.First)) {
                                    break;
                                }
                            }
                            for (let i = Math.min(indexBlurred - 1, this.registeredControls.length - 1); i >= 0; i--) {
                                if (this.registeredControls[i].focus(exports.FocusPosition.First)) {
                                    break;
                                }
                            }
                            break;
                        case exports.BlurReconciliation.Prev:
                            for (let i = Math.min(indexBlurred - 1, this.registeredControls.length - 1); i >= 0; i--) {
                                if (this.registeredControls[i].focus(exports.FocusPosition.Last)) {
                                    break;
                                }
                            }
                            for (let i = Math.max(indexBlurred + (this.registeredControls[indexBlurred] === childBlurred ? 1 : 0), 0); i < this.registeredControls.length; i++) {
                                if (this.registeredControls[i].focus(exports.FocusPosition.Last)) {
                                    break;
                                }
                            }
                            break;
                    }
                }
            }
            if (this.props.requireChildFocus && this.getFocusedIndex() < 0) {
                this.blur();
            }
        }
        /**
         * Reconciles the focus state of this control's children when this control is focused with no focused children after
         * a child has been blurred.
         * @param index The index of the child control that was blurred. If the child is no longer registered, then this is
         * the index of the child prior to being unregistered.
         * @param child The child control that was blurred.
         * @returns The index of the child control to focus.
         */
        reconcileChildBlur(index, child) {
            var _a;
            if (this.props.reconcileChildBlur) {
                return this.props.reconcileChildBlur(index, child);
            }
            if (((_a = this.registeredControls) === null || _a === void 0 ? void 0 : _a[index]) !== child) {
                return exports.BlurReconciliation.Next;
            }
            else {
                return -1;
            }
        }
        /**
         * Sets the component to be disabled, removing the ability for the component to scroll. Setting
         * a component to disabled will also blur the component and its children, if necessary.
         * @param isDisabled Whether or not the component is disabled.
         */
        setDisabled(isDisabled) {
            this._isDisabled = isDisabled;
            if (isDisabled) {
                this.blur();
                this.onDisabled(this);
            }
            else {
                this.onEnabled(this);
            }
        }
        /**
         * Registers a child control with this control.
         * @param control The control to register.
         * @param index The index at which to register the control. If none is provided,
         * the control will be registered at the end of the collection of child controls.
         */
        register(control, index) {
            if (this.registeredControls === undefined) {
                this.registeredControls = [];
            }
            if (index !== undefined) {
                this.registeredControls.splice(index, 0, control);
                if (this.focusedIndex >= index) {
                    this.focusedIndex++;
                }
            }
            else {
                this.registeredControls.push(control);
            }
            control.setParent(this);
            control.onRegistered(control);
        }
        /**
         * Unregisters a child control with this control.
         * @param item The child control or index of a child control to unregister. If a
         * child control is provided, it will attempt to be located in the control's
         * child registry and then removed. If an index is provided, the child control
         * at that registered index will be removed.
         */
        unregister(item) {
            if (this.registeredControls !== undefined) {
                let index = -1;
                if (typeof item === 'number') {
                    index = item;
                }
                else {
                    index = this.registeredControls.indexOf(item);
                }
                if (index >= 0 && index < this.length) {
                    const controlToRemove = this.registeredControls[index];
                    const isRemovedControlFocused = controlToRemove._isFocused;
                    this.registeredControls.splice(index, 1);
                    controlToRemove.parent = undefined;
                    if (isRemovedControlFocused) {
                        controlToRemove.blur();
                        this.handleNoFocusedChild(index, controlToRemove);
                    }
                    else {
                        if (this.focusedIndex === index) {
                            this.focusedIndex = -1;
                        }
                        else if (this.focusedIndex > index) {
                            this.focusedIndex--;
                        }
                    }
                    this.focusedIndex = Math.min(this.focusedIndex, this.registeredControls.length - 1);
                    if (controlToRemove.length > 0) {
                        controlToRemove.clearRegistered();
                    }
                    controlToRemove.onUnregistered(controlToRemove);
                    if (this.length === 0) {
                        this.registeredControls = undefined;
                    }
                }
            }
        }
        /**
         * Clears the list of registered components.
         */
        clearRegistered() {
            if (this.registeredControls !== undefined) {
                const registeredControls = this.registeredControls;
                this.registeredControls = undefined;
                this.focusedIndex = -1;
                for (let i = 0; i < registeredControls.length; i++) {
                    const controlToRemove = registeredControls[i];
                    controlToRemove.parent = undefined;
                    if (controlToRemove._isFocused) {
                        controlToRemove.blur();
                    }
                    if (controlToRemove.length > 0) {
                        controlToRemove.clearRegistered();
                    }
                    registeredControls[i].onUnregistered(registeredControls[i]);
                }
                // Only call this once for the last child removed to prevent multiple sequential, redundant reconciliations.
                this.handleNoFocusedChild(0, registeredControls[registeredControls.length - 1]);
            }
        }
        /**
         * Gets the current focused index in the registered controls collection.
         * @returns The index of the focused control in the collection of registered controls.
         */
        getFocusedIndex() {
            var _a, _b;
            return ((_b = (_a = this.registeredControls) === null || _a === void 0 ? void 0 : _a[this.focusedIndex]) === null || _b === void 0 ? void 0 : _b._isFocused) ? this.focusedIndex : -1;
        }
        /**
         * Gets the most recent focused index (including the current focused index, if one exists) in the registered controls
         * collection.
         * @returns The index of the most recently focused control in the collection of registered controls.
         */
        getMostRecentFocusedIndex() {
            return this.focusedIndex;
        }
        /**
         * Sets the current most recently focused child control index. If this control is focused and has children
         * that have focus, this will also switch child focus to the new index.
         * @param index The index of the child control to set most recent focus for.
         * @param focusPosition The focus position to focus the child for, if required.
         */
        setFocusedIndex(index, focusPosition = exports.FocusPosition.MostRecent) {
            var _a;
            if (this.isFocused && this.length > 0 && ((_a = this.registeredControls) === null || _a === void 0 ? void 0 : _a.findIndex(c => c.isFocused)) !== -1) {
                const child = this.getChild(index);
                if (child !== undefined) {
                    child.focus(focusPosition);
                }
            }
            else if (this.length > 0 && index >= 0 && index < this.length) {
                this.focusedIndex = index;
            }
        }
        /**
         * Gets a child control at the specified index.
         * @param index The index of the child control to get.
         * @returns The specified child control.
         */
        getChild(index) {
            if (this.registeredControls !== undefined) {
                return this.registeredControls[index];
            }
            return undefined;
        }
        /**
         * Gets the index of a specified child control within the registered
         * child controls collection.
         * @param child The child to get the index of.
         * @returns The index of the child, or -1 if not found.
         */
        indexOf(child) {
            if (this.registeredControls !== undefined) {
                return this.registeredControls.indexOf(child);
            }
            return -1;
        }
        /**
         * Sets the parent of this control.
         * @param parent The parent to set.
         */
        setParent(parent) {
            this.parent = parent;
        }
        /**
         * Sets whether or not this control is in scroll isolation. While scroll isolation
         * is enabled, scroll events will not propagate to the control's parent when the
         * control has focus.
         * @param isolated Whether or not the control is isolated.
         */
        setIsolated(isolated) {
            this._isIsolated = isolated;
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            exports.FSComponent.visitNodes(thisNode, (node) => {
                const instance = node.instance;
                if (instance !== this && (instance === null || instance === void 0 ? void 0 : instance._UICONTROL_)) {
                    this.register(node.instance);
                    return true;
                }
                return false;
            });
        }
        /**
         * Renders the control.
         * @returns The component VNode.
         */
        render() {
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null, this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            super.destroy();
            this.props.onDestroyed && this.props.onDestroyed(this);
        }
    }

    /** Scroll utils */
    class ScrollUtils {
        /**
         * Scrolls the container to make sure an element is in view.
         * @param el The element to scroll into view in the container.
         * @param container The container to scroll.
         * @param pinDirection The end of the container which the element should be pinned to.
         */
        static ensureInView(el, container, pinDirection = 'none') {
            const cHeight = container.offsetHeight;
            const eHeight = el.offsetHeight;
            if (eHeight > cHeight) {
                return;
            }
            const cTop = container.scrollTop;
            const cBottom = cTop + cHeight;
            try {
                const eTop = ScrollUtils.findOffsetTopRelativeToAncestor(el, container);
                const eBottom = eTop + eHeight;
                if (!this.isElementInViewport(cTop, cBottom, eTop, eBottom) || pinDirection !== 'none') {
                    if ((eTop < cTop) || pinDirection === 'top') {
                        container.scrollTop -= (cTop - eTop);
                    }
                    else if ((eBottom > cBottom) || pinDirection === 'bottom') {
                        container.scrollTop += (eBottom - cBottom);
                    }
                }
            }
            catch (e) {
                console.warn(e);
            }
        }
        /**
         * Finds the offsetTop of an element relative to one of its ancestors.
         * @param element An element.
         * @param ancestor An ancestor of `element`.
         * @returns the offsetTop of the element relative to the ancestor.
         * @throws Error if the offsetTop could not be calculated.
         */
        static findOffsetTopRelativeToAncestor(element, ancestor) {
            const ancestorParent = ancestor.offsetParent;
            let top = element.offsetTop;
            while (element.offsetParent !== ancestorParent) {
                if (!(element.offsetParent instanceof HTMLElement)) {
                    throw new Error('Element\'s offset ancestry does not directly lead to the specified ancestor');
                }
                element = element.offsetParent;
                top += element.offsetTop;
            }
            return top - ancestor.offsetTop;
        }
        /**
         * Checks if an element is visible.
         * @param cTop The top coordinate of the scroll container.
         * @param cBottom The bottom coordinate of the scroll container.
         * @param eTop The top coordinate of the element.
         * @param eBottom The bottom coordinate of the element.
         * @returns A boolean.
         */
        static isElementInViewport(cTop, cBottom, eTop, eBottom) {
            return eTop >= cTop && eBottom <= cBottom;
        }
    }

    /**
     * A component that displays a collection of UiControls in a list format.
     */
    class HardwareUiControlList extends HardwareUiControl {
        /**
         * Creates an instance of a ControlList.
         * @param props The props on the ControlList component.
         */
        constructor(props) {
            super(props);
            this.el = exports.FSComponent.createRef();
            this.itemsContainer = exports.FSComponent.createRef();
            /**
             * Sorts the registered controls by the provided ordering comparison function.
             * @param a The first control to compare.
             * @param b The second control to compare.
             * @returns Negative if the first control is less than, zero if equal, positive if greater than.
             */
            this.sortControls = (a, b) => {
                if (this.controlToDataMap !== undefined && this.props.orderBy !== undefined) {
                    const aData = this.controlToDataMap.get(a);
                    const bData = this.controlToDataMap.get(b);
                    if (aData !== undefined && bData !== undefined) {
                        return this.props.orderBy(aData, bData);
                    }
                }
                return 0;
            };
            if (props.orderBy !== undefined) {
                this.dataToControlMap = new Map();
                this.controlToElementMap = new Map();
                this.controlToDataMap = new Map();
                this.currentControlOrder = [];
            }
        }
        /** @inheritdoc */
        onAfterRender(node) {
            super.onAfterRender(node);
            if (this.props.itemSize !== undefined && this.props.numItems !== undefined) {
                const listSizePx = (this.props.itemSize * this.props.numItems).toFixed(4);
                this.el.instance.style.height = listSizePx;
                this.itemsContainer.instance.style.height = listSizePx;
            }
            this.renderList();
            this.props.data.sub(this.onDataChanged.bind(this));
        }
        /**
         * A callback fired when the array subject data changes.
         * @param index The index of the change.
         * @param type The type of change.
         * @param data The item that was changed.
         */
        onDataChanged(index, type, data) {
            switch (type) {
                case exports.SubscribableArrayEventType.Added:
                    this.onDataAdded(index, data);
                    break;
                case exports.SubscribableArrayEventType.Removed:
                    this.onDataRemoved(index, data);
                    break;
                case exports.SubscribableArrayEventType.Cleared:
                    this.onDataCleared();
                    break;
            }
        }
        /**
         * An event called when data is added to the subscription.
         * @param index The index that the data was added at.
         * @param data The data that was added.
         */
        onDataAdded(index, data) {
            if (data !== undefined) {
                const currentItemElement = this.itemsContainer.instance.children.item(index);
                if (Array.isArray(data)) {
                    for (let i = 0; i < data.length; i++) {
                        const dataItem = data[i];
                        const indexToAdd = index + i;
                        this.addDataItem(dataItem, indexToAdd, currentItemElement);
                    }
                }
                else {
                    this.addDataItem(data, index, currentItemElement);
                }
            }
            this.updateOrder();
        }
        /**
         * Adds a data item to the control list and performs the required rendering and
         * ordering operations.
         * @param dataItem The data item to add to the list.
         * @param indexToAdd The index to add the item at.
         * @param currentItemElement The current DOM element that resides at the location to add to.
         */
        addDataItem(dataItem, indexToAdd, currentItemElement) {
            const controlNode = this.props.renderItem(dataItem, indexToAdd);
            const control = controlNode.instance;
            //Nefariously monkey-patch the onFocused handler to get notified when the item is focused,
            //regardless of the underlying implementation or overrides
            const originalOnFocused = control.onFocused.bind(control);
            control.onFocused = (source) => {
                this.onItemFocused();
                originalOnFocused && originalOnFocused(source);
            };
            const element = this.renderToDom(controlNode, indexToAdd, currentItemElement);
            this.register(controlNode.instance, indexToAdd >= 0 ? indexToAdd : undefined);
            if (element !== null && controlNode.instance !== null) {
                this.addToOrderTracking(controlNode.instance, dataItem, element);
            }
            this.props.scrollToMostRecentlyAdded && this.scrollToIndex(indexToAdd);
        }
        /**
         * An event called when data is removed from the subscription.
         * @param index The index that the data was removed at.
         * @param data The data that was removed;
         */
        onDataRemoved(index, data) {
            if (index >= 0 && index < this.length) {
                if (Array.isArray(data)) {
                    for (let i = 0; i < data.length; i++) {
                        const dataItem = data[i];
                        this.removeDataItem(dataItem, index);
                    }
                }
                else if (data !== undefined) {
                    this.removeDataItem(data, index);
                }
                this.updateOrder();
            }
        }
        /**
         * Removes a data item from the control list.
         * @param data The data item to remove.
         * @param index The index of the data that was removed.
         */
        removeDataItem(data, index) {
            if (this.dataToControlMap !== undefined && this.registeredControls !== undefined) {
                const control = this.dataToControlMap.get(data);
                if (control !== undefined) {
                    index = this.registeredControls.indexOf(control);
                }
            }
            let control;
            if (this.registeredControls !== undefined) {
                control = this.registeredControls[index];
            }
            this.unregister(index);
            this.removeDomNode(index);
            this.removeFromOrderTracking(data);
            control === null || control === void 0 ? void 0 : control.destroy();
        }
        /**
         * An event called when the data is cleared in the subscription.
         */
        onDataCleared() {
            let controls;
            if (this.registeredControls !== undefined) {
                controls = [...this.registeredControls];
            }
            this.clearRegistered();
            this.itemsContainer.instance.innerHTML = '';
            this.clearOrderTracking();
            if (controls !== undefined) {
                for (let i = 0; i < controls.length; i++) {
                    controls[i].destroy();
                }
            }
            if (this.props.onItemSelected) {
                this.props.onItemSelected(null, null, -1);
            }
        }
        /**
         * Adds a data item to element order tracking information.
         * @param control The index to add the data item at.
         * @param data The data to add tracking information for.
         * @param element The DOM element to associate with this data item.
         */
        addToOrderTracking(control, data, element) {
            if (this.controlToElementMap !== undefined && this.dataToControlMap !== undefined && this.controlToDataMap !== undefined) {
                this.dataToControlMap.set(data, control);
                this.controlToElementMap.set(control, element);
                this.controlToDataMap.set(control, data);
            }
        }
        /**
         * Removes a data item from element order tracking information.
         * @param data The data item to remove order tracking information for.
         */
        removeFromOrderTracking(data) {
            if (this.controlToElementMap !== undefined && this.dataToControlMap !== undefined && this.controlToDataMap !== undefined) {
                const control = this.dataToControlMap.get(data);
                if (control !== undefined) {
                    this.dataToControlMap.delete(data);
                    this.controlToElementMap.delete(control);
                    this.controlToDataMap.delete(control);
                }
            }
        }
        /**
         * Clears all data item element order tracking information.
         */
        clearOrderTracking() {
            if (this.controlToElementMap !== undefined && this.dataToControlMap !== undefined && this.controlToDataMap !== undefined) {
                this.dataToControlMap.clear();
                this.controlToElementMap.clear();
                this.controlToDataMap.clear();
            }
        }
        /**
         * Updates the order of data items in the list by the props supplied
         * comparison function, if one exists.
         */
        updateOrder() {
            if (this.controlToElementMap !== undefined && this.dataToControlMap !== undefined && this.controlToDataMap !== undefined) {
                const itemsContainer = this.itemsContainer.instance;
                if (this.registeredControls !== undefined) {
                    const selectedControl = this.getChild(this.getFocusedIndex());
                    this.registeredControls.sort(this.sortControls);
                    if (!this.orderUnchanged()) {
                        for (let i = 0; i < this.registeredControls.length; i++) {
                            const element = this.controlToElementMap.get(this.registeredControls[i]);
                            if (element !== undefined) {
                                itemsContainer.appendChild(element);
                            }
                        }
                        this.currentControlOrder = [...this.registeredControls];
                        if (selectedControl !== undefined) {
                            this.focusedIndex = this.registeredControls.indexOf(selectedControl);
                            this.ensureIndexInView(this.focusedIndex);
                        }
                    }
                }
            }
        }
        /**
         * Checks whether or not the control order is the same as it was previously.
         * @returns True if the order is the same, false otherwise.
         */
        orderUnchanged() {
            if (this.registeredControls !== undefined && this.currentControlOrder !== undefined) {
                if (this.registeredControls.length === this.currentControlOrder.length) {
                    return this.registeredControls.every((control, i) => this.currentControlOrder && control === this.currentControlOrder[i]);
                }
                return false;
            }
            return true;
        }
        /**
         * Removes a dom node from the collection at the specified index.
         * @param index The index to remove.
         */
        removeDomNode(index) {
            const child = this.itemsContainer.instance.childNodes.item(index);
            this.itemsContainer.instance.removeChild(child);
        }
        /**
         * Adds a list rendered dom node to the collection.
         * @param node Item to render and add.
         * @param index The index to add at.
         * @param el The element to add to.
         * @returns The created DOM element.
         */
        renderToDom(node, index, el) {
            if (el !== null) {
                node && el && exports.FSComponent.renderBefore(node, el);
                return el.previousElementSibling;
            }
            else {
                el = this.itemsContainer.instance;
                node && el && exports.FSComponent.render(node, el);
                return this.itemsContainer.instance.lastElementChild;
            }
        }
        /**
         * Scrolls to an item.
         * @param index is the index of the list item to scroll to.
         * @param focusPosition The focus position to apply to children of the item being scrolled to.
         */
        scrollToIndex(index, focusPosition = exports.FocusPosition.First) {
            const control = this.getChild(index);
            if (control !== undefined) {
                control.focus(focusPosition);
            }
        }
        /**
         * Ensures an indexed list item is in view.
         * @param index The index of the list item.
         * @param pinDirection The end of the container which the element should be pinned to.
         */
        ensureIndexInView(index, pinDirection = 'none') {
            var _a, _b;
            const el = this.getElement(index);
            const container = (_b = (_a = this.props.scrollContainer) === null || _a === void 0 ? void 0 : _a.getOrDefault()) !== null && _b !== void 0 ? _b : this.itemsContainer.getOrDefault();
            if (el && container && !this.props.disableContainerScroll) {
                ScrollUtils.ensureInView(el, container, pinDirection);
            }
        }
        /**
         * Gets an element at the specified data/control index.
         * @param index The data/control index to get the element for.
         * @returns The request HTML element.
         */
        getElement(index) {
            var _a;
            return (_a = this.itemsContainer.instance.children[index]) !== null && _a !== void 0 ? _a : null;
        }
        /**
         * Gets the data object related to the selected DOM element.
         * @param index The index of the data to get.
         * @returns The selected item, if found.
         */
        getData(index) {
            var _a;
            const control = this.getChild(index);
            if (this.controlToDataMap !== undefined && control !== undefined) {
                return (_a = this.controlToDataMap.get(control)) !== null && _a !== void 0 ? _a : null;
            }
            if (index > -1) {
                return this.props.data.get(index);
            }
            return null;
        }
        /**
         * Get the selected HTMLElement.
         * @returns The selected element, if found.
         */
        getSelectedElement() {
            var _a;
            return (_a = this.itemsContainer.instance.children[this.getSelectedIndex()]) !== null && _a !== void 0 ? _a : null;
        }
        /**
         * Gets the index of the currently selected element.
         * @returns Selected element index. Returns -1 if nothing found.
         */
        getSelectedIndex() {
            if (this.length > 0) {
                return this.getFocusedIndex();
            }
            return -1;
        }
        /**
         * Gets the instance of the node at the specified index.
         * @param index The index to get the instance for.
         * @returns The node instance of specified type.
         */
        getChildInstance(index) {
            const child = this.getChild(index);
            if (child !== undefined) {
                return child;
            }
            return null;
        }
        /** @inheritdoc */
        onBlurred(source) {
            if (this.props.onItemSelected) {
                this.props.onItemSelected(null, null, -1);
            }
            super.onBlurred(source);
        }
        /**
         * Responds to when a list item is focused.
         */
        onItemFocused() {
            const index = this.getFocusedIndex();
            this.ensureIndexInView(index);
            if (this.props.onItemSelected) {
                const control = this.getChild(index);
                if (control !== undefined && control.isFocused) {
                    let data = this.props.data.get(index);
                    if (this.controlToDataMap !== undefined) {
                        data = this.controlToDataMap.get(control);
                    }
                    if (data !== undefined) {
                        this.props.onItemSelected(data, control, index);
                    }
                }
            }
        }
        /**
         * Renders the complete list of data items as control components.
         */
        renderList() {
            this.itemsContainer.instance.textContent = '';
            this.onDataAdded(0, this.props.data.getArray());
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { class: `ui-control-list ${(_a = this.props.class) !== null && _a !== void 0 ? _a : ''}`, ref: this.el },
                exports.FSComponent.buildComponent("div", { ref: this.itemsContainer, class: 'ui-control-list-content' }),
                !this.props.hideScrollbar && this.renderScrollbar()));
        }
    }

    /**
     * A base component for horizon layers.
     */
    class HorizonLayer extends DisplayComponent {
        constructor() {
            super(...arguments);
            this._isAttached = false;
            this._isVisible = true;
        }
        /**
         * Checks whether this layer is attached to a horizon component.
         * @returns Whether this layer is attached to a horizon component.
         */
        isAttached() {
            return this._isAttached;
        }
        /**
         * Checks whether this layer is visible.
         * @returns whether this layer is visible.
         */
        isVisible() {
            return this._isVisible;
        }
        /**
         * Sets this layer's visibility.
         * @param val Whether this layer should be visible.
         */
        setVisible(val) {
            if (this._isVisible === val) {
                return;
            }
            this._isVisible = val;
            if (this._isAttached) {
                this.onVisibilityChanged(val);
            }
        }
        /**
         * This method is called when this layer's visibility changes.
         * @param isVisible Whether the layer is now visible.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            // noop
        }
        /**
         * This method is called when this layer is attached to its parent horizon component.
         */
        onAttached() {
            this._isAttached = true;
            if (!this._isVisible) {
                this.onVisibilityChanged(this._isVisible);
            }
        }
        /**
         * This method is called when this layer's parent horizon component is awakened.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this layer's parent horizon component is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when this layer's horizon projection changes.
         * @param projection This layer's horizon projection.
         * @param changeFlags The types of changes made to the projection.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onProjectionChanged(projection, changeFlags) {
            // noop
        }
        /**
         * This method is called once every update cycle.
         * @param time The current time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            // noop
        }
        /**
         * This method is called when this layer is detached from its parent horizon component.
         */
        onDetached() {
            this._isAttached = false;
        }
    }

    /**
     * An implementation of MapCanvasLayerCanvasInstance.
     */
    class HorizonCanvasLayerCanvasInstanceClass {
        /**
         * Creates a new canvas instance.
         * @param canvas The canvas element.
         * @param context The canvas 2D rendering context.
         * @param isDisplayed Whether the canvas is displayed.
         */
        constructor(canvas, context, isDisplayed) {
            this.canvas = canvas;
            this.context = context;
            this.isDisplayed = isDisplayed;
        }
        /** @inheritdoc */
        clear() {
            this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        }
        /** @inheritdoc */
        reset() {
            const width = this.canvas.width;
            this.canvas.width = 0;
            this.canvas.width = width;
        }
    }
    /**
     * A layer which uses a canvas to draw graphics.
     */
    class HorizonCanvasLayer extends HorizonLayer {
        constructor() {
            super(...arguments);
            this.displayCanvasRef = exports.FSComponent.createRef();
            this.width = 0;
            this.height = 0;
            this.displayCanvasContext = null;
            this.isInit = false;
        }
        /**
         * Gets this layer's display canvas instance.
         * @returns This layer's display canvas instance.
         * @throws Error if this layer's display canvas instance has not been initialized.
         */
        get display() {
            if (!this._display) {
                throw new Error('HorizonCanvasLayer: attempted to access display before it was initialized');
            }
            return this._display;
        }
        /**
         * Gets this layer's buffer canvas instance.
         * @returns This layer's buffer canvas instance.
         * @throws Error if this layer's buffer canvas instance has not been initialized.
         */
        get buffer() {
            if (!this._buffer) {
                throw new Error('HorizonCanvasLayer: attempted to access buffer before it was initialized');
            }
            return this._buffer;
        }
        /**
         * Attempts to get this layer's display canvas instance.
         * @returns This layer's display canvas instance, or undefined if it has not been initialized.
         */
        tryGetDisplay() {
            return this._display;
        }
        /**
         * Attempts to get this layer's buffer canvas instance.
         * @returns This layer's buffer canvas instance, or undefined if it has not been initialized.
         */
        tryGetBuffer() {
            return this._buffer;
        }
        /**
         * Gets the width of the canvas element, in pixels.
         * @returns the width of the canvas element.
         */
        getWidth() {
            return this.width;
        }
        /**
         * Gets the height of the canvas element, in pixels.
         * @returns the height of the canvas element.
         */
        getHeight() {
            return this.height;
        }
        /**
         * Sets the width of the canvas element, in pixels.
         * @param width The new width.
         */
        setWidth(width) {
            if (width === this.width) {
                return;
            }
            this.width = width;
            if (this.isInit) {
                this.updateCanvasSize();
            }
        }
        /**
         * Sets the height of the canvas element, in pixels.
         * @param height The new height.
         */
        setHeight(height) {
            if (height === this.height) {
                return;
            }
            this.height = height;
            if (this.isInit) {
                this.updateCanvasSize();
            }
        }
        /**
         * Copies the contents of the buffer to the display. Has no effect if this layer does not have a buffer.
         */
        copyBufferToDisplay() {
            if (!this.isInit || !this.props.useBuffer) {
                return;
            }
            this.display.context.drawImage(this.buffer.canvas, 0, 0, this.width, this.height);
        }
        /**
         * A callback called after the component renders.
         */
        onAfterRender() {
            this.displayCanvasContext = this.displayCanvasRef.instance.getContext('2d');
        }
        /** @inheritdoc */
        onVisibilityChanged() {
            if (this.isInit) {
                this.updateCanvasVisibility();
            }
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.initCanvasInstances();
            this.isInit = true;
            this.updateCanvasVisibility();
            this.updateCanvasSize();
        }
        /**
         * Initializes this layer's canvas instances.
         */
        initCanvasInstances() {
            this._display = this.createCanvasInstance(this.displayCanvasRef.instance, this.displayCanvasContext, true);
            if (this.props.useBuffer) {
                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d');
                this._buffer = this.createCanvasInstance(canvas, context, false);
            }
        }
        /**
         * Creates a canvas instance.
         * @param canvas The canvas element.
         * @param context The canvas 2D rendering context.
         * @param isDisplayed Whether the canvas is displayed.
         * @returns a canvas instance.
         */
        createCanvasInstance(canvas, context, isDisplayed) {
            return new HorizonCanvasLayerCanvasInstanceClass(canvas, context, isDisplayed);
        }
        /**
         * Updates the canvas element's size.
         */
        updateCanvasSize() {
            const displayCanvas = this.display.canvas;
            displayCanvas.width = this.width;
            displayCanvas.height = this.height;
            displayCanvas.style.width = `${this.width}px`;
            displayCanvas.style.height = `${this.height}px`;
            if (this._buffer) {
                const bufferCanvas = this._buffer.canvas;
                bufferCanvas.width = this.width;
                bufferCanvas.height = this.height;
            }
        }
        /**
         * Updates the visibility of the display canvas.
         */
        updateCanvasVisibility() {
            this.display.canvas.style.display = this.isVisible() ? 'block' : 'none';
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("canvas", { ref: this.displayCanvasRef, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '', width: '0', height: '0', style: 'position: absolute;' }, " "));
        }
    }

    /**
     * An implementation of {@link SubEventInterface}.
     */
    class SubEvent {
        constructor() {
            this.subs = [];
            this.notifyDepth = 0;
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /** @inheritdoc */
        on(handler, paused = false) {
            const sub = new HandlerSubscription(handler, undefined, this.onSubDestroyedFunc);
            this.subs.push(sub);
            if (paused) {
                sub.pause();
            }
            return sub;
        }
        /** @inheritdoc */
        clear() {
            this.notifyDepth++;
            for (let i = 0; i < this.subs.length; i++) {
                this.subs[i].destroy();
            }
            this.notifyDepth--;
            if (this.notifyDepth === 0) {
                this.subs.length = 0;
            }
        }
        /** @inheritdoc */
        notify(sender, data) {
            let needCleanUpSubs = false;
            this.notifyDepth++;
            const subLen = this.subs.length;
            for (let i = 0; i < subLen; i++) {
                try {
                    const sub = this.subs[i];
                    if (!sub.isPaused) {
                        sub.handler(sender, data);
                    }
                    needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                }
                catch (error) {
                    console.error(`SubEvent: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs && this.notifyDepth === 0) {
                this.subs = this.subs.filter(sub => sub.isAlive);
            }
        }
        /**
         * Responds to when a subscription to this event is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                this.subs.splice(this.subs.indexOf(sub), 1);
            }
        }
    }

    /**
     * The different types of horizon projection changes.
     */
    exports.HorizonProjectionChangeType = void 0;
    (function (HorizonProjectionChangeType) {
        HorizonProjectionChangeType[HorizonProjectionChangeType["Position"] = 1] = "Position";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Altitude"] = 2] = "Altitude";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Heading"] = 4] = "Heading";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Pitch"] = 8] = "Pitch";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Roll"] = 16] = "Roll";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Offset"] = 32] = "Offset";
        HorizonProjectionChangeType[HorizonProjectionChangeType["ProjectedSize"] = 64] = "ProjectedSize";
        HorizonProjectionChangeType[HorizonProjectionChangeType["Fov"] = 128] = "Fov";
        HorizonProjectionChangeType[HorizonProjectionChangeType["FovEndpoints"] = 256] = "FovEndpoints";
        HorizonProjectionChangeType[HorizonProjectionChangeType["PitchScaleFactor"] = 512] = "PitchScaleFactor";
        HorizonProjectionChangeType[HorizonProjectionChangeType["HeadingScaleFactor"] = 1024] = "HeadingScaleFactor";
        HorizonProjectionChangeType[HorizonProjectionChangeType["ScaleFactor"] = 2048] = "ScaleFactor";
        HorizonProjectionChangeType[HorizonProjectionChangeType["ProjectedOffset"] = 4096] = "ProjectedOffset";
        HorizonProjectionChangeType[HorizonProjectionChangeType["OffsetCenterProjected"] = 8192] = "OffsetCenterProjected";
    })(exports.HorizonProjectionChangeType || (exports.HorizonProjectionChangeType = {}));
    /**
     * A perspective projection from the point of view of an airplane.
     */
    class HorizonProjection {
        /**
         * Constructor.
         * @param projectedWidth The initial projected width of the projection, in pixels.
         * @param projectedHeight The initial projected height of the projection, in pixels.
         * @param fov The initial field of view of the projection, in degrees.
         */
        constructor(projectedWidth, projectedHeight, fov) {
            this.position = new GeoPoint(0, 0);
            this.altitude = 0;
            this.heading = 0;
            this.roll = 0;
            this.pitch = 0;
            this.offset = Vec3Math.create();
            this.fovEndpoints = VecNMath.create(4, 0.5, 0, 0.5, 1);
            this.scaleFactor = 1;
            this.projectedOffset = Vec2Math.create();
            this.offsetCenterProjected = Vec2Math.create();
            this.positionAngleTransforms = [new Transform3D(), new Transform3D()];
            this.altitudeTransform = new Transform3D();
            this.positionAltitudeTransforms = [...this.positionAngleTransforms, this.altitudeTransform];
            this.positionTransform = new Transform3D();
            this.planeAngles = Vec3Math.create();
            this.planeAngleTransforms = [new Transform3D(), new Transform3D(), new Transform3D()];
            this.planeTransform = new Transform3D();
            this.cameraPos = Vec3Math.create();
            this.surfacePos = Vec3Math.create();
            this.perspectiveTransform = new TransformPerspective();
            this.oldParameters = {
                position: new GeoPoint(0, 0),
                altitude: 0,
                heading: 0,
                pitch: 0,
                roll: 0,
                offset: Vec3Math.create(),
                projectedSize: Vec2Math.create(),
                fov: 0,
                fovEndpoints: VecNMath.create(4),
                pitchScaleFactor: 1,
                headingScaleFactor: 1,
                scaleFactor: 1,
                projectedOffset: Vec2Math.create(),
                offsetCenterProjected: Vec2Math.create()
            };
            this.queuedParameters = {};
            this.updateQueued = false;
            this.changeEvent = new SubEvent();
            this.projectedSize = Vec2Math.create(projectedWidth, projectedHeight);
            this.fov = fov;
            this.pitchScaleFactor = 1;
            this.headingScaleFactor = 1;
            this.recompute();
        }
        /**
         * Gets the position of this projection.
         * @returns The position of this projection.
         */
        getPosition() {
            return this.position.readonly;
        }
        /**
         * Gets the altitude of this projection, in meters above mean sea level.
         * @returns The altitude of this projection, in meters above mean sea level.
         */
        getAltitude() {
            return this.altitude;
        }
        /**
         * Gets the true heading of this projection, in degrees.
         * @returns The true heading of this projection, in degrees.
         */
        getHeading() {
            return this.heading;
        }
        /**
         * Gets the pitch of this projection, in degrees.
         * @returns The pitch of this projection, in degrees.
         */
        getPitch() {
            return this.pitch;
        }
        /**
         * Gets the roll of this projection, in degrees.
         * @returns The roll of this projection, in degrees.
         */
        getRoll() {
            return this.roll;
        }
        /**
         * Gets the size of the projected window, as `[width, height]` in pixels.
         * @returns The size of the projected window, as `[width, height]` in pixels.
         */
        getProjectedSize() {
            return this.projectedSize;
        }
        /**
         * Gets the field of view of this projection, in degrees.
         * @returns The field of view of this projection, in degrees.
         */
        getFov() {
            return this.fov;
        }
        /**
         * Gets the projected endpoints at which the field of view is measured, as `[x1, y1, x2, y2]`, with each component
         * expressed relative to the width or height of the projected window.
         * @returns The projected endpoints at which the field of view is measured, as `[x1, y1, x2, y2]`, with each
         * component expressed relative to the width or height of the projected window.
         */
        getFovEndpoints() {
            return this.fovEndpoints;
        }
        /**
         * Gets the focal length of this projection, in meters. The focal length is set such that one meter at a distance
         * from the camera equal to the focal length subtends an angle equal to the field of view.
         * @returns The focal length of this projection, in meters.
         */
        getFocalLength() {
            return this.surfacePos[2];
        }
        /**
         * Gets the pitch angle scale factor of this projection. When a point is projected, its relative zero-roll pitch
         * angle is scaled by this value before projection. The relative zero-roll pitch angle of a point is the pitch angle
         * of the point relative to the camera if the airplane had zero degrees of roll. A scaling factor of 1 leaves points
         * unchanged. Factors less than 1 cause points to be projected closer to the pitch line corresponding to the
         * airplane's pitch. Factors greater than 1 cause points to be projected farther from the pitch line corresponding
         * to the airplane's pitch.
         * @returns The pitch angle scale factor of this projection.
         */
        getPitchScaleFactor() {
            return this.pitchScaleFactor;
        }
        /**
         * Gets the heading angle scale factor of this projection. When a point is projected, its relative heading angle is
         * scaled by this value before projection. The relative heading angle of a point is the difference between the
         * bearing of the point from the airplane and the airplane's heading. A scaling factor of 1 leaves points unchanged.
         * Factors less than 1 cause points to be projected closer to the lubber line. Factors greater than 1 cause points to
         * be projected farther from the lubber line.
         * @returns The heading angle scale factor of this projection.
         */
        getHeadingScaleFactor() {
            return this.headingScaleFactor;
        }
        /**
         * Gets the nominal scale factor of this projection. At a distance from the camera equal to the focal length, one
         * meter will be projected to a number of pixels equal to the nominal scale factor.
         * @returns The nominal scale factor of this projection.
         */
        getScaleFactor() {
            return this.scaleFactor;
        }
        /**
         * Gets the projected offset of this projection's center, as `[x, y]` in pixels.
         * @returns The projected offset of this projection's center, as `[x, y]` in pixels.
         */
        getProjectedOffset() {
            return this.projectedOffset;
        }
        /**
         * Gets the projected center of this projection, including offset, as `[x, y]` in pixels.
         * @returns The projected center of this projection, including offset, as `[x, y]` in pixels.
         */
        getOffsetCenterProjected() {
            return this.offsetCenterProjected;
        }
        /**
         * Recomputes this projection's computed parameters.
         */
        recompute() {
            Vec2Math.set(this.projectedSize[0] / 2 + this.projectedOffset[0], this.projectedSize[1] / 2 + this.projectedOffset[1], this.offsetCenterProjected);
            // Compute the transformation required to bring the position of the plane to [0, 0, 0]. After applying this
            // transformation, with a heading/roll/pitch of 0, the positive z axis points in the direction of the plane, the
            // positive x axis points directly upward (away from the ground), and the positive y axis points to the right.
            this.positionAngleTransforms[0].toRotationZ(-this.position.lon * Avionics.Utils.DEG2RAD);
            this.positionAngleTransforms[1].toRotationY(this.position.lat * Avionics.Utils.DEG2RAD);
            this.altitudeTransform.toTranslation(-(UnitType.GA_RADIAN.convertTo(1, UnitType.METER) + this.altitude), 0, 0);
            Transform3D.concat(this.positionTransform, this.positionAltitudeTransforms);
            this.planeAngles[0] = -this.heading * Avionics.Utils.DEG2RAD;
            this.planeAngles[1] = this.pitch * Avionics.Utils.DEG2RAD;
            this.planeAngles[2] = this.roll * Avionics.Utils.DEG2RAD;
            this.planeAngleTransforms[0].toRotationZ(this.planeAngles[2]);
            this.planeAngleTransforms[1].toRotationY(this.planeAngles[1]);
            this.planeAngleTransforms[2].toRotationX(this.planeAngles[0]);
            Transform3D.concat(this.planeTransform, this.planeAngleTransforms);
            // Convert camera offset to world coordinates
            this.planeTransform.apply(this.offset, this.cameraPos);
            this.scaleFactor = Math.hypot(this.fovEndpoints[2] * this.projectedSize[0] - this.fovEndpoints[0] * this.projectedSize[0], this.fovEndpoints[3] * this.projectedSize[1] - this.fovEndpoints[1] * this.projectedSize[1]);
            this.surfacePos[2] = 1 / (2 * Math.tan(this.fov * 0.5 * Avionics.Utils.DEG2RAD));
            this.perspectiveTransform
                .setCameraRotation(this.planeTransform)
                .setSurfacePosition(this.surfacePos);
        }
        /**
         * Sets this projection's parameters. Parameters not explicitly defined in the parameters argument will be left
         * unchanged.
         * @param parameters The new parameters.
         */
        set(parameters) {
            var _a, _b, _c, _d, _e, _f, _g;
            // save old values
            this.storeParameters(this.oldParameters);
            parameters.position !== undefined && this.position.set(parameters.position);
            this.altitude = (_a = parameters.altitude) !== null && _a !== void 0 ? _a : this.altitude;
            this.heading = (_b = parameters.heading) !== null && _b !== void 0 ? _b : this.heading;
            this.pitch = (_c = parameters.pitch) !== null && _c !== void 0 ? _c : this.pitch;
            this.roll = (_d = parameters.roll) !== null && _d !== void 0 ? _d : this.roll;
            parameters.offset !== undefined && this.offset.set(parameters.offset);
            parameters.projectedSize !== undefined && this.projectedSize.set(parameters.projectedSize);
            this.fov = (_e = parameters.fov) !== null && _e !== void 0 ? _e : this.fov;
            parameters.fovEndpoints !== undefined && this.fovEndpoints.set(parameters.fovEndpoints);
            this.pitchScaleFactor = (_f = parameters.pitchScaleFactor) !== null && _f !== void 0 ? _f : this.pitchScaleFactor;
            this.headingScaleFactor = (_g = parameters.headingScaleFactor) !== null && _g !== void 0 ? _g : this.headingScaleFactor;
            parameters.projectedOffset !== undefined && this.projectedOffset.set(parameters.projectedOffset);
            let changeFlags = this.computeChangeFlags(this.oldParameters);
            if ((changeFlags & HorizonProjection.RECOMPUTE_MASK) !== 0) {
                this.recompute();
                changeFlags |= this.computeDerivedChangeFlags(this.oldParameters);
            }
            if (changeFlags !== 0) {
                this.changeEvent.notify(this, changeFlags);
            }
        }
        /**
         * Sets the projection parameters to be applied when `applyQueued()` is called.
         * @param parameters The parameter changes to queue.
         */
        setQueued(parameters) {
            Object.assign(this.queuedParameters, parameters);
            this.updateQueued = true;
        }
        /**
         * Applies the set of queued projection changes, if any are queued.
         */
        applyQueued() {
            if (this.updateQueued) {
                this.updateQueued = false;
                this.set(this.queuedParameters);
                for (const key in this.queuedParameters) {
                    delete this.queuedParameters[key];
                }
            }
        }
        /**
         * Stores this projection's current parameters into a record.
         * @param record The record in which to store the parameters.
         */
        storeParameters(record) {
            record.position.set(this.position);
            record.altitude = this.altitude;
            record.heading = this.heading;
            record.pitch = this.pitch;
            record.roll = this.roll;
            record.offset.set(this.offset);
            record.projectedSize.set(this.projectedSize);
            record.fov = this.fov;
            record.fovEndpoints.set(this.fovEndpoints);
            record.pitchScaleFactor = this.pitchScaleFactor;
            record.headingScaleFactor = this.headingScaleFactor;
            record.scaleFactor = this.scaleFactor;
            record.projectedOffset.set(this.projectedOffset);
            record.offsetCenterProjected.set(this.offsetCenterProjected);
        }
        /**
         * Computes change flags given a set of old parameters.
         * @param oldParameters The old parameters.
         * @returns Change flags based on the specified old parameters.
         */
        computeChangeFlags(oldParameters) {
            return (oldParameters.position.equals(this.position) ? 0 : exports.HorizonProjectionChangeType.Position)
                | (oldParameters.altitude === this.altitude ? 0 : exports.HorizonProjectionChangeType.Altitude)
                | (oldParameters.heading === this.heading ? 0 : exports.HorizonProjectionChangeType.Heading)
                | (oldParameters.pitch === this.pitch ? 0 : exports.HorizonProjectionChangeType.Pitch)
                | (oldParameters.roll === this.roll ? 0 : exports.HorizonProjectionChangeType.Roll)
                | (Vec3Math.equals(oldParameters.offset, this.offset) ? 0 : exports.HorizonProjectionChangeType.Offset)
                | (Vec2Math.equals(oldParameters.projectedSize, this.projectedSize) ? 0 : exports.HorizonProjectionChangeType.ProjectedSize)
                | (oldParameters.fov === this.fov ? 0 : exports.HorizonProjectionChangeType.Fov)
                | (VecNMath.equals(oldParameters.fovEndpoints, this.fovEndpoints) ? 0 : exports.HorizonProjectionChangeType.FovEndpoints)
                | (oldParameters.pitchScaleFactor === this.pitchScaleFactor ? 0 : exports.HorizonProjectionChangeType.PitchScaleFactor)
                | (oldParameters.headingScaleFactor === this.headingScaleFactor ? 0 : exports.HorizonProjectionChangeType.HeadingScaleFactor)
                | (Vec2Math.equals(oldParameters.projectedOffset, this.projectedOffset) ? 0 : exports.HorizonProjectionChangeType.ProjectedOffset);
        }
        /**
         * Computes change flags for derived parameters given a set of old parameters.
         * @param oldParameters The old parameters.
         * @returns Change flags for derived parameters based on the specified old parameters.
         */
        computeDerivedChangeFlags(oldParameters) {
            return (oldParameters.scaleFactor === this.scaleFactor ? 0 : exports.HorizonProjectionChangeType.ScaleFactor)
                | (Vec2Math.equals(oldParameters.offsetCenterProjected, this.offsetCenterProjected) ? 0 : exports.HorizonProjectionChangeType.OffsetCenterProjected);
        }
        /**
         * Subscribes a change listener to this projection. The listener will be called every time this projection changes.
         * A listener can be subscribed multiple times; it will be called once for every time it is registered.
         * @param listener The change listener to subscribe.
         * @returns The new subscription.
         */
        onChange(listener) {
            return this.changeEvent.on(listener);
        }
        /**
         * Projects a point represented by a set of lat/lon coordinates and altitude.
         * @param position The lat/lon coordinates of the point to project.
         * @param altitude The altitude of the point to project, in meters.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        project(position, altitude, out) {
            const vec = GeoPoint.sphericalToCartesian(position, HorizonProjection.vec3Cache[0]);
            Vec3Math.multScalar(vec, UnitType.GA_RADIAN.convertTo(1, UnitType.METER) + altitude, vec);
            this.positionTransform.apply(vec, vec);
            return this.projectRelativeVec(vec, out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        projectRelativeCoordinates(arg1, arg2, arg3, arg4) {
            if (typeof arg1 === 'number') {
                return this.projectRelativeVec(Vec3Math.set(arg1, arg2, arg3, HorizonProjection.vec3Cache[0]), arg4);
            }
            else {
                return this.projectRelativeVec(arg1, arg2);
            }
        }
        /**
         * Projects a point relative to the position of the airplane in spherical space.
         * @param bearing The true bearing from the airplane to the point to project, in degrees.
         * @param distance The geodetic horizontal distance from the point to project to the airplane, in meters.
         * @param height The geodetic height of the point to project relative to the airplane, in meters.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        projectRelativeSpherical(bearing, distance, height, out) {
            return this.project(this.position.offset(bearing, UnitType.METER.convertTo(distance, UnitType.GA_RADIAN), HorizonProjection.geoPointCache[0]), this.altitude + height, out);
        }
        /**
         * Projects a point relative to the position of the airplane in Euclidean space. The point ot project is expressed
         * in terms of bearing, horizontal distance, and height. The coordinate system is defined at the position of the
         * airplane, with the vertical axis perpendicular to the surface of the Earth and the horizontal plane parallel to
         * the Earth's surface at the point directly underneath the airplane.
         * @param bearing The true bearing from the airplane to the point to project, in degrees.
         * @param distance The Euclidean horizontal distance from the point to project to the airplane, in meters.
         * @param height The Euclidean height of the point to project relative to the airplane, in meters.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        projectRelativeEuclidean(bearing, distance, height, out) {
            const vec = Vec2Math.setFromPolar(distance, bearing * Avionics.Utils.DEG2RAD, HorizonProjection.vec3Cache[0]);
            const x = height;
            const y = vec[1];
            const z = vec[0];
            return this.projectRelativeVec(Vec3Math.set(x, y, z, vec), out);
        }
        /**
         * Projects a point relative to the position of the airplane in Euclidean space. The point to project is expressed
         * in terms of distance, relative bearing and pitch. The coordinate system is defined at the position of the
         * airplane, with the vertical axis perpendicular to the surface of the Earth and the horizontal plane parallel to
         * the Earth's surface at the point directly underneath the airplane.
         * @param distance The Euclidean distance from the point to project to the airplane, in meters.
         * @param bearing The relative bearing from the airplane to the point to project, in degrees. The relative bearing is
         * measured relative to the airplane's heading, with positive angles sweeping clockwise when viewed from above.
         * @param pitch The pitch angle from the airplane to the point to project, in degrees. The pitch angle is measured
         * relative to the horizontal plane, with positive angles sweeping above the plane.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        projectRelativeAngular(distance, bearing, pitch, out) {
            const trueBearing = this.heading + bearing;
            const vec = Vec3Math.setFromSpherical(distance, (90 - pitch) * Avionics.Utils.DEG2RAD, trueBearing * Avionics.Utils.DEG2RAD, HorizonProjection.vec3Cache[0]);
            const x = vec[2];
            const y = vec[1];
            const z = vec[0];
            return this.projectRelativeVec(Vec3Math.set(x, y, z, vec), out);
        }
        /**
         * Projects a 3D vector defined relative to the airplane, as `[x, y, z]` in meters with the coordinate system
         * defined as follows for an airplane with heading/roll/pitch of zero degrees:
         * * The positive z axis points in the direction of the airplane.
         * * The positive x axis points directly upward.
         * * The positive y axis points to the right.
         * @param vec The vector to project.
         * @param out The 2D vector to which to write the result.
         * @returns The projected vector.
         */
        projectRelativeVec(vec, out) {
            const vecToProject = Vec3Math.sub(vec, this.cameraPos, HorizonProjection.relativeVec3Cache[0]);
            if (this.pitchScaleFactor !== 1 || this.headingScaleFactor !== 1) {
                this.applyPitchHeadingScale(vecToProject, this.pitchScaleFactor, this.headingScaleFactor, vecToProject);
            }
            this.perspectiveTransform.apply(vecToProject, out);
            return Vec2Math.set(out[1] * this.scaleFactor + this.offsetCenterProjected[0], -out[0] * this.scaleFactor + this.offsetCenterProjected[1], out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        projectCameraRelativeCoordinates(arg1, arg2, arg3, arg4) {
            if (typeof arg1 === 'number') {
                return this.projectCameraRelativeVec(Vec3Math.set(arg1, arg2, arg3, HorizonProjection.vec3Cache[0]), arg4);
            }
            else {
                return this.projectCameraRelativeVec(arg1, arg2);
            }
        }
        /**
         * Projects a point relative to the position of the projection camera in Euclidean space. The point to project is
         * expressed in terms of bearing, horizontal distance, and height. The coordinate system is defined at the position
         * of the camera, with the vertical axis perpendicular to the surface of the Earth and the horizontal plane parallel
         * to the Earth's surface at the point directly underneath the camera.
         * @param bearing The true bearing from the camera to the point to project, in degrees.
         * @param distance The Euclidean horizontal distance from the point to project to the camera, in meters.
         * @param height The Euclidean height of the point to project relative to the camera, in meters.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        projectCameraRelativeEuclidean(bearing, distance, height, out) {
            const vec = Vec2Math.setFromPolar(distance, bearing * Avionics.Utils.DEG2RAD, HorizonProjection.vec3Cache[0]);
            const x = height;
            const y = vec[1];
            const z = vec[0];
            return this.projectCameraRelativeVec(Vec3Math.set(x, y, z, vec), out);
        }
        /**
         * Projects a point relative to the position of the projection camera in Euclidean space. The point to project is
         * expressed in terms of distance, bearing and pitch. The coordinate system is defined at the position of the camera,
         * with the vertical axis perpendicular to the surface of the Earth and the horizontal plane parallel to the Earth's
         * surface at the point directly underneath the camera.
         * @param distance The Euclidean distance from the point to project to the camera, in meters.
         * @param bearing The true bearing from the camera to the point to project, in degrees.
         * @param pitch The pitch angle from the camera to the point to project, in degrees. The pitch angle is measured
         * relative to the horizontal plane, with positive angles sweeping above the plane.
         * @param out The 2D vector to which to write the result.
         * @returns The projected point, as `[x, y]` in pixels.
         */
        projectCameraRelativeAngular(distance, bearing, pitch, out) {
            const vec = Vec3Math.setFromSpherical(distance, (90 - pitch) * Avionics.Utils.DEG2RAD, bearing * Avionics.Utils.DEG2RAD, HorizonProjection.vec3Cache[0]);
            const x = vec[2];
            const y = vec[1];
            const z = vec[0];
            return this.projectCameraRelativeVec(Vec3Math.set(x, y, z, vec), out);
        }
        /**
         * Projects a 3D vector defined relative to the camera, as `[x, y, z]` in meters with the coordinate system
         * defined as follows for an airplane with heading/roll/pitch of zero degrees:
         * * The positive z axis points in the direction of the airplane.
         * * The positive x axis points directly upward.
         * * The positive y axis points to the right.
         * @param vec The vector to project.
         * @param out The 2D vector to which to write the result.
         * @returns The projected vector.
         */
        projectCameraRelativeVec(vec, out) {
            if (this.pitchScaleFactor !== 1 || this.headingScaleFactor !== 1) {
                vec = this.applyPitchHeadingScale(vec, this.pitchScaleFactor, this.headingScaleFactor, HorizonProjection.cameraRelativeVec3Cache[0]);
            }
            this.perspectiveTransform.apply(vec, out);
            return Vec2Math.set(out[1] * this.scaleFactor + this.offsetCenterProjected[0], -out[0] * this.scaleFactor + this.offsetCenterProjected[1], out);
        }
        /**
         * Inverts a pair of projected coordinates to calculate the pitch angle and true bearing (both relative to the
         * horizontal plane with origin at the camera) of the set of points in 3D space that are projected to those
         * coordinates.
         * @param x The projected x coordinate to invert.
         * @param y The projected y coordinate to invert.
         * @param out The 2D vector to which to write the result.
         * @returns The pitch angle and true bearing (both relative to the horizontal plane with origin at the camera) of the
         * set of points in 3D space that are projected to the specified coordinates, as `[pitch angle, true bearing]` in
         * degrees.
         */
        invertToCameraRelativeAngles(x, y, out) {
            // We need to invert the perspective transformation. Since we are only calculating the pitch and bearing angles
            // referenced to the camera (and not also the distance), we can achieve this by choosing an arbitrary z-value for
            // the inverted position vector. If we choose the z-value to be equal to the camera's focal length, then the x and
            // y values of the inverted position vector will be exactly the x and y values of its projection.
            const perspectiveX = (this.offsetCenterProjected[1] - y) / this.scaleFactor;
            const perspectiveY = (x - this.offsetCenterProjected[0]) / this.scaleFactor;
            const cameraRelativeVec = Vec3Math.set(perspectiveX, perspectiveY, this.surfacePos[2], HorizonProjection.vec3Cache[0]);
            this.planeTransform.apply(cameraRelativeVec, cameraRelativeVec);
            if (this.pitchScaleFactor !== 1 || this.headingScaleFactor !== 1) {
                this.applyPitchHeadingScale(cameraRelativeVec, 1 / this.pitchScaleFactor, 1 / this.headingScaleFactor, cameraRelativeVec);
            }
            // Rotate the coordinate system such that z is the vertical axis and x/y form the horizontal plane so that we can
            // use standard theta/phi angles.
            Vec3Math.set(cameraRelativeVec[1], cameraRelativeVec[2], cameraRelativeVec[0], cameraRelativeVec);
            return Vec2Math.set(((90 - Vec3Math.theta(cameraRelativeVec) * Avionics.Utils.RAD2DEG) + 180) % 360 - 180, // -180 to 180
            ((90 - Vec3Math.phi(cameraRelativeVec) * Avionics.Utils.RAD2DEG) + 360) % 360, // 0 to 360
            out);
        }
        /**
         * Applies pitch and heading angle scaling to a 3D vector defined relative to the camera, as `[x, y, z]` in meters
         * with the coordinate system defined as follows for an airplane with heading/roll/pitch of zero degrees:
         * * The positive z axis points in the direction of the airplane.
         * * The positive x axis points directly upward.
         * * The positive y axis points to the right.
         * @param vec The vector to project.
         * @param pitchScaleFactor The pitch angle scale factor to use.
         * @param headingScaleFactor The heading angle scale factor to use.
         * @param out The 3D vector to which to write the result.
         * @returns The scaled vector.
         */
        applyPitchHeadingScale(vec, pitchScaleFactor, headingScaleFactor, out) {
            if (pitchScaleFactor !== 1 || headingScaleFactor !== 1) {
                // Rotate the coordinate system such that z is the vertical axis and x/y form the horizontal plane so that we can
                // use standard theta/phi angles.
                Vec3Math.set(vec[1], vec[2], vec[0], out);
                const length = Vec3Math.abs(out);
                if (length > 0) {
                    let theta;
                    let phi;
                    if (pitchScaleFactor !== 1) {
                        theta = Vec3Math.theta(out);
                        const planePitchRad = this.planeAngles[1];
                        const relativePitchAngle = ((MathUtils.HALF_PI - theta) - planePitchRad + Math.PI) % MathUtils.TWO_PI - Math.PI; // range -pi to +pi
                        if (relativePitchAngle !== 0) {
                            theta = MathUtils.HALF_PI - (relativePitchAngle * pitchScaleFactor + planePitchRad);
                            phi = Vec3Math.phi(out);
                            Vec3Math.setFromSpherical(length, theta, phi, out);
                        }
                    }
                    if (headingScaleFactor !== 1) {
                        phi !== null && phi !== void 0 ? phi : (phi = Vec3Math.phi(out));
                        const planeHeadingRad = -this.planeAngles[0];
                        const relativeHeadingAngle = ((MathUtils.HALF_PI - phi) - planeHeadingRad + Math.PI) % MathUtils.TWO_PI - Math.PI; // range -pi to +pi
                        if (relativeHeadingAngle !== 0) {
                            phi = MathUtils.HALF_PI - (relativeHeadingAngle * headingScaleFactor + planeHeadingRad);
                            theta !== null && theta !== void 0 ? theta : (theta = Vec3Math.theta(out));
                            Vec3Math.setFromSpherical(length, theta, phi, out);
                        }
                    }
                    // Rotate back to the original coordinate system.
                    return Vec3Math.set(out[2], out[0], out[1], out);
                }
            }
            return Vec3Math.copy(vec, out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        isInProjectedBounds(point, arg2, arg3) {
            let bounds;
            if (point instanceof Float64Array) {
                bounds = arg2;
            }
            else {
                point = this.project(point, arg2, HorizonProjection.vec2Cache[0]);
                bounds = arg3;
            }
            const x = point[0];
            const y = point[1];
            if (!isFinite(x) || !isFinite(y)) {
                return false;
            }
            let left;
            let top;
            let right;
            let bottom;
            if (bounds) {
                left = bounds[0];
                top = bounds[1];
                right = bounds[2];
                bottom = bounds[3];
            }
            else {
                left = 0;
                top = 0;
                right = this.projectedSize[0];
                bottom = this.projectedSize[1];
            }
            return x >= left && x <= right && y >= top && y <= bottom;
        }
    }
    HorizonProjection.RECOMPUTE_MASK = ~(exports.HorizonProjectionChangeType.PitchScaleFactor | exports.HorizonProjectionChangeType.HeadingScaleFactor);
    HorizonProjection.vec2Cache = [Vec2Math.create()];
    HorizonProjection.vec3Cache = [Vec3Math.create()];
    HorizonProjection.geoPointCache = [new GeoPoint(0, 0)];
    HorizonProjection.relativeVec3Cache = [Vec3Math.create()];
    HorizonProjection.cameraRelativeVec3Cache = [Vec3Math.create()];

    /**
     * A canvas horizon layer whose size and position is synced with the horizon projection window.
     */
    class HorizonSyncedCanvasLayer extends HorizonCanvasLayer {
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.updateFromProjectedSize(this.props.projection.getProjectedSize());
        }
        /**
         * Updates this layer according to the current size of the horizon projected window.
         * @param projectedSize The size of the horizon projected window.
         */
        updateFromProjectedSize(projectedSize) {
            this.setWidth(projectedSize[0]);
            this.setHeight(projectedSize[1]);
            const displayCanvas = this.display.canvas;
            displayCanvas.style.left = '0px';
            displayCanvas.style.top = '0px';
        }
        /** @inheritdoc */
        onProjectionChanged(projection, changeFlags) {
            if (BitFlags.isAll(changeFlags, exports.HorizonProjectionChangeType.ProjectedSize)) {
                this.updateFromProjectedSize(projection.getProjectedSize());
            }
        }
    }

    /* eslint-disable @typescript-eslint/no-non-null-assertion */
    /**
     * A horizon layer containing a single canvas synced to the horizon's projected size that can be shared amongst
     * multiple sublayers for rendering.
     *
     * All of the layer's children are rendered on top of the shared canvas element. All children that extend
     * {@link HorizonSharedCanvasSubLayer} are treated as sublayers and can render to the shared canvas element.
     */
    class HorizonSharedCanvasLayer extends HorizonLayer {
        constructor() {
            super(...arguments);
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.sublayers = [];
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            this.canvasLayerRef.instance.setVisible(isVisible);
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].setVisible(isVisible);
            }
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            this.thisNode = thisNode;
            // Enumerate sublayers
            exports.FSComponent.visitNodes(thisNode, node => {
                if (node !== thisNode && node.instance instanceof DisplayComponent) {
                    if (node.instance instanceof HorizonSharedCanvasSubLayer) {
                        this.sublayers.push(node.instance);
                    }
                    return true;
                }
                return false;
            });
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.canvasLayerRef.instance.onAttached();
            this.sharedCanvasInstance = new HorizonSharedCanvasInstanceClass(this.canvasLayerRef.instance.display);
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].attach(this.props.projection, this.sharedCanvasInstance);
            }
        }
        /** @inheritdoc */
        onWake() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onWake();
            }
        }
        /** @inheritdoc */
        onSleep() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onSleep();
            }
        }
        /** @inheritdoc */
        onProjectionChanged(projection, changeFlags) {
            this.canvasLayerRef.instance.onProjectionChanged(projection, changeFlags);
            if (BitFlags.isAll(changeFlags, exports.HorizonProjectionChangeType.ProjectedSize)) {
                this.sharedCanvasInstance.invalidate();
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onProjectionChanged(projection, changeFlags);
            }
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            let invalidate = false;
            for (let i = 0; !invalidate && i < this.sublayers.length; i++) {
                invalidate = this.sublayers[i].shouldInvalidate(time, elapsed);
            }
            if (invalidate) {
                this.sharedCanvasInstance.invalidate();
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onUpdated(time, elapsed);
            }
            this.sharedCanvasInstance.revalidate();
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null,
                exports.FSComponent.buildComponent(HorizonSyncedCanvasLayer, { ref: this.canvasLayerRef, projection: this.props.projection }),
                this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            this.thisNode && exports.FSComponent.shallowDestroy(this.thisNode);
            super.destroy();
        }
    }
    /**
     * A sublayer of {@link HorizonSharedCanvasLayer}.
     */
    class HorizonSharedCanvasSubLayer extends DisplayComponent {
        constructor() {
            super(...arguments);
            this._isAttached = false;
            this._isVisible = true;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's horizon projection.
         * @throws Error if this sublayer is not attached.
         */
        get projection() {
            if (this._projection) {
                return this._projection;
            }
            throw new Error('HorizonSharedCanvasSubLayer: attempted to access projection before sublayer was attached');
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's shared canvas instance.
         * @throws Error if this sublayer is not attached.
         */
        get display() {
            if (this._display) {
                return this._display;
            }
            throw new Error('HorizonSharedCanvasSubLayer: attempted to access display canvas before sublayer was attached');
        }
        /**
         * Checks whether this sublayer is attached to a parent layer.
         * @returns Whether this sublayer is attached to a parent layer.
         */
        isAttached() {
            return this._isAttached;
        }
        /**
         * Checks whether this sublayer is visible.
         * @returns Whether this sublayer is visible.
         */
        isVisible() {
            return this._isVisible;
        }
        /**
         * Attaches this sublayer to a parent layer.
         * @param projection The horizon projection used by this sublayer.
         * @param display The canvas instance shared by this sublayer.
         */
        attach(projection, display) {
            this._projection = projection;
            this._display = display;
            this._isAttached = true;
            if (!this._isVisible) {
                this.onVisibilityChanged(this._isVisible);
            }
            this.onAttached();
        }
        /**
         * Sets this sublayer's visibility.
         * @param val Whether this sublayer should be visible.
         */
        setVisible(val) {
            if (this._isVisible === val) {
                return;
            }
            this._isVisible = val;
            if (this._isAttached) {
                this.onVisibilityChanged(val);
            }
        }
        /**
         * This method is called when this layer's visibility changes.
         * @param isVisible Whether the layer is now visible.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            // noop
        }
        /**
         * This method is called when this sublayer is attached to its parent layer.
         */
        onAttached() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is awakened.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when this sublayer's horizon projection changes.
         * @param projection This sublayer's horizon projection.
         * @param changeFlags The types of changes made to the projection.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onProjectionChanged(projection, changeFlags) {
            // noop
        }
        /**
         * This method is called at the beginning of every update cycle to check whether this sublayer's shared canvas
         * instance should be invalidated. If the canvas is already invalidated, then this method will not be called.
         * @param time The current time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         * @returns Whether this sublayer's shared canvas instance should be invalidated.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        shouldInvalidate(time, elapsed) {
            return false;
        }
        /**
         * This method is called once every update cycle after this sublayer's shared canvas instance has had a chance to be
         * invalidated.
         * @param time The current time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            // noop
        }
        /** @inheritdoc */
        render() {
            return null;
        }
    }
    /**
     * An implementation of {@link HorizonSharedCanvasInstance} which is backed by a
     * {@link HorizonCanvasLayerCanvasInstance}.
     */
    class HorizonSharedCanvasInstanceClass {
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether this canvas has been invalidated. */
        get isInvalidated() {
            return this._isInvalidated;
        }
        /**
         * Creates a new instance of HorizonSharedCanvasInstanceClass.
         * @param instance This instance's backing canvas instance.
         */
        constructor(instance) {
            this.instance = instance;
            /** @inheritdoc */
            this.canvas = this.instance.canvas;
            /** @inheritdoc */
            this.context = this.instance.context;
            this._isInvalidated = false;
        }
        /**
         * Invalidates and clears this canvas.
         */
        invalidate() {
            this._isInvalidated = true;
            this.instance.clear();
        }
        /** @inheritdoc */
        revalidate() {
            this._isInvalidated = false;
        }
    }

    /**
     * A component which displays an artificial horizon. A horizon tracks the position, altitude, heading, pitch, and roll
     * of an airplane and uses a persepctive projection to project points in space to a planar pixel grid. Each horizon
     * component maintains a {@link HorizonComponent} instance which handles the details of the projection.
     * {@link HorizonLayer} objects added to the horizon as children determine what is drawn in the horizon window.
     */
    class HorizonComponent extends DisplayComponent {
        /** @inheritdoc */
        constructor(props) {
            var _a;
            super(props);
            this.layerEntries = [];
            this.lastUpdateTime = 0;
            this._isAwake = true;
            this.projectedSize = SubscribableUtils.toSubscribable(this.props.projectedSize, true);
            this.fov = SubscribableUtils.toSubscribable(this.props.fov, true);
            if (this.props.fovEndpoints !== undefined) {
                this.fovEndpoints = SubscribableUtils.toSubscribable(this.props.fovEndpoints, true);
            }
            if (this.props.projectedOffset !== undefined) {
                this.projectedOffset = SubscribableUtils.toSubscribable(this.props.projectedOffset, true);
            }
            const initialSize = this.projectedSize.get();
            const initialFov = this.fov.get();
            if (this.props.projection !== undefined) {
                this.props.projection.set({ projectedSize: initialSize, fov: initialFov });
            }
            this.projection = (_a = this.props.projection) !== null && _a !== void 0 ? _a : new HorizonProjection(initialSize[0], initialSize[1], initialFov);
        }
        /**
         * Gets the size of this map's projected window, in pixels.
         * @returns The size of this map's projected window.
         */
        getProjectedSize() {
            return this.projection.getProjectedSize();
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether this horizon is awake.
         */
        get isAwake() {
            return this._isAwake;
        }
        /**
         * Puts this horizon to sleep. While asleep, this horizon will not be updated.
         */
        sleep() {
            this.setAwakeState(false);
        }
        /**
         * Wakes this horizon, allowing it to be updated.
         */
        wake() {
            this.setAwakeState(true);
        }
        /**
         * Sets this horizon's awake state. If the new awake state is the same as the current state, nothing will happen.
         * Otherwise, this horizon's layers will be notified that the map has either been woken or put to sleep.
         * @param isAwake The new awake state.
         */
        setAwakeState(isAwake) {
            if (this._isAwake === isAwake) {
                return;
            }
            this._isAwake = isAwake;
            this._isAwake ? this.onWake() : this.onSleep();
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            var _a, _b;
            this.projection.onChange(this.onProjectionChanged.bind(this));
            this.projectedSizeSub = this.projectedSize.sub(size => {
                this.projection.set({ projectedSize: size });
            }, true);
            this.fovSub = this.fov.sub(fov => {
                this.projection.set({ fov });
            }, true);
            this.fovEndpointsSub = (_a = this.fovEndpoints) === null || _a === void 0 ? void 0 : _a.sub(fovEndpoints => {
                this.projection.set({ fovEndpoints });
            }, true);
            this.projectedOffsetSub = (_b = this.projectedOffset) === null || _b === void 0 ? void 0 : _b.sub(projectedOffset => {
                this.projection.set({ projectedOffset });
            }, true);
            this.attachLayers(thisNode);
            if (!this._isAwake) {
                this.sleepLayers();
            }
        }
        /**
         * Scans this component's VNode sub-tree for HorizonLayer components and attaches them when found. Only the top-most
         * level of HorizonLayer components are attached; layers that are themselves children of other layers are not
         * attached.
         * @param thisNode This component's VNode.
         */
        attachLayers(thisNode) {
            exports.FSComponent.visitNodes(thisNode, node => {
                if (node.instance instanceof HorizonLayer) {
                    this.attachLayer(node.instance);
                    return true;
                }
                return false;
            });
        }
        /**
         * This method is called when this horizon is awakened.
         */
        onWake() {
            this.wakeLayers();
        }
        /**
         * Calls the onWake() method of this horizon's layers.
         */
        wakeLayers() {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onWake();
            }
        }
        /**
         * This method is called when this horizon is put to sleep.
         */
        onSleep() {
            this.sleepLayers();
        }
        /**
         * Calls the onSleep() method of this horizon's layers.
         */
        sleepLayers() {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onSleep();
            }
        }
        /**
         * This method is called when this horizon's projection changes.
         * @param projection This horizon's projection.
         * @param changeFlags The types of changes made to the projection.
         */
        onProjectionChanged(projection, changeFlags) {
            if (BitFlags.isAll(changeFlags, exports.HorizonProjectionChangeType.ProjectedSize)) {
                this.onProjectedSizeChanged();
            }
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onProjectionChanged(projection, changeFlags);
            }
        }
        /**
         * This method is called when the size of this horizon's projected window changes.
         */
        onProjectedSizeChanged() {
            // noop
        }
        /**
         * Attaches a layer to this horizon component. If the layer is already attached, then this method has no effect.
         * @param layer The layer to attach.
         */
        attachLayer(layer) {
            if (this.layerEntries.findIndex(entry => entry.layer === layer) >= 0) {
                return;
            }
            const entry = new LayerEntry$1(layer);
            this.layerEntries.push(entry);
            entry.attach();
        }
        /**
         * Detaches a layer from this horizon component.
         * @param layer The layer to detach.
         * @returns Whether the layer was succesfully detached.
         */
        detachLayer(layer) {
            const index = this.layerEntries.findIndex(entry => entry.layer === layer);
            if (index >= 0) {
                const entry = this.layerEntries[index];
                entry.detach();
                this.layerEntries.splice(index, 1);
                return true;
            }
            else {
                return false;
            }
        }
        /**
         * Updates this horizon.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         */
        update(time) {
            if (!this._isAwake) {
                return;
            }
            this.onUpdated(time, time - this.lastUpdateTime);
            this.lastUpdateTime = time;
        }
        /**
         * This method is called once every update cycle.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        onUpdated(time, elapsed) {
            this.updateLayers(time, elapsed);
        }
        /**
         * Updates this horizon's attached layers.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        updateLayers(time, elapsed) {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].update(time);
            }
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }, this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b, _c, _d;
            super.destroy();
            (_a = this.projectedSizeSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.fovSub) === null || _b === void 0 ? void 0 : _b.destroy();
            (_c = this.fovEndpointsSub) === null || _c === void 0 ? void 0 : _c.destroy();
            (_d = this.projectedOffsetSub) === null || _d === void 0 ? void 0 : _d.destroy();
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].destroy();
            }
        }
    }
    /**
     * An entry for a horizon layer.
     */
    let LayerEntry$1 = class LayerEntry {
        /**
         * Constructor.
         * @param layer This entry's map layer.
         */
        constructor(layer) {
            this.layer = layer;
            this.updatePeriod = 0;
            this.lastUpdated = 0;
        }
        /**
         * Attaches this layer entry.
         */
        attach() {
            var _a, _b;
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.updateFreqSub = (_b = this.layer.props.updateFreq) === null || _b === void 0 ? void 0 : _b.sub((freq) => {
                const clamped = Math.max(0, freq);
                this.updatePeriod = clamped === 0 ? 0 : 1000 / clamped;
            }, true);
            this.layer.onAttached();
        }
        /**
         * Updates this layer entry.
         * @param currentTime The current time as a UNIX timestamp.
         */
        update(currentTime) {
            if (currentTime - this.lastUpdated >= this.updatePeriod) {
                this.layer.onUpdated(currentTime, currentTime - this.lastUpdated);
                this.lastUpdated = currentTime;
            }
        }
        /**
         * Detaches this layer entry.
         */
        detach() {
            var _a;
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.layer.onDetached();
        }
        /**
         * Destroys this layer entry. This will detach this entry's layer and destroy it.
         */
        destroy() {
            this.detach();
            this.layer.destroy();
        }
    };

    /**
     * Types of facilities.
     */
    exports.FacilityType = void 0;
    (function (FacilityType) {
        FacilityType["Airport"] = "APT";
        FacilityType["Intersection"] = "INT";
        FacilityType["VOR"] = "VOR";
        FacilityType["NDB"] = "NDB";
        FacilityType["USR"] = "USR";
        FacilityType["RWY"] = "RWY";
        FacilityType["VIS"] = "VIS";
    })(exports.FacilityType || (exports.FacilityType = {}));
    /**
     * The available facility frequency types.
     */
    exports.FacilityFrequencyType = void 0;
    (function (FacilityFrequencyType) {
        FacilityFrequencyType[FacilityFrequencyType["None"] = 0] = "None";
        FacilityFrequencyType[FacilityFrequencyType["ATIS"] = 1] = "ATIS";
        FacilityFrequencyType[FacilityFrequencyType["Multicom"] = 2] = "Multicom";
        FacilityFrequencyType[FacilityFrequencyType["Unicom"] = 3] = "Unicom";
        FacilityFrequencyType[FacilityFrequencyType["CTAF"] = 4] = "CTAF";
        FacilityFrequencyType[FacilityFrequencyType["Ground"] = 5] = "Ground";
        FacilityFrequencyType[FacilityFrequencyType["Tower"] = 6] = "Tower";
        FacilityFrequencyType[FacilityFrequencyType["Clearance"] = 7] = "Clearance";
        FacilityFrequencyType[FacilityFrequencyType["Approach"] = 8] = "Approach";
        FacilityFrequencyType[FacilityFrequencyType["Departure"] = 9] = "Departure";
        FacilityFrequencyType[FacilityFrequencyType["Center"] = 10] = "Center";
        FacilityFrequencyType[FacilityFrequencyType["FSS"] = 11] = "FSS";
        FacilityFrequencyType[FacilityFrequencyType["AWOS"] = 12] = "AWOS";
        FacilityFrequencyType[FacilityFrequencyType["ASOS"] = 13] = "ASOS";
        /** Clearance Pre-Taxi*/
        FacilityFrequencyType[FacilityFrequencyType["CPT"] = 14] = "CPT";
        /** Remote Clearance Delivery */
        FacilityFrequencyType[FacilityFrequencyType["GCO"] = 15] = "GCO";
    })(exports.FacilityFrequencyType || (exports.FacilityFrequencyType = {}));
    exports.LandingSystemCategory = void 0;
    (function (LandingSystemCategory) {
        LandingSystemCategory[LandingSystemCategory["None"] = 0] = "None";
        LandingSystemCategory[LandingSystemCategory["Cat1"] = 1] = "Cat1";
        LandingSystemCategory[LandingSystemCategory["Cat2"] = 2] = "Cat2";
        LandingSystemCategory[LandingSystemCategory["Cat3"] = 3] = "Cat3";
        LandingSystemCategory[LandingSystemCategory["Localizer"] = 4] = "Localizer";
        LandingSystemCategory[LandingSystemCategory["Igs"] = 5] = "Igs";
        LandingSystemCategory[LandingSystemCategory["LdaNoGs"] = 6] = "LdaNoGs";
        LandingSystemCategory[LandingSystemCategory["LdaWithGs"] = 7] = "LdaWithGs";
        LandingSystemCategory[LandingSystemCategory["SdfNoGs"] = 8] = "SdfNoGs";
        LandingSystemCategory[LandingSystemCategory["SdfWithGs"] = 9] = "SdfWithGs";
    })(exports.LandingSystemCategory || (exports.LandingSystemCategory = {}));
    /** Additional Approach Types (additive to those defined in simplane). */
    exports.AdditionalApproachType = void 0;
    (function (AdditionalApproachType) {
        AdditionalApproachType[AdditionalApproachType["APPROACH_TYPE_VISUAL"] = 99] = "APPROACH_TYPE_VISUAL";
    })(exports.AdditionalApproachType || (exports.AdditionalApproachType = {}));
    /**
     * Names for approach types.
     */
    exports.ApproachTypeName = void 0;
    (function (ApproachTypeName) {
        ApproachTypeName["Undefined"] = "";
        ApproachTypeName["Gps"] = "GPS";
        ApproachTypeName["Vor"] = "VOR";
        ApproachTypeName["Ndb"] = "NDB";
        ApproachTypeName["Ils"] = "ILS";
        ApproachTypeName["Loc"] = "LOC";
        ApproachTypeName["Sdf"] = "SDF";
        ApproachTypeName["Lda"] = "LDA";
        ApproachTypeName["VorDme"] = "VORDME";
        ApproachTypeName["NdbDme"] = "NDBDME";
        ApproachTypeName["Rnav"] = "RNAV";
        ApproachTypeName["LocBackcourse"] = "BLOC";
        ApproachTypeName["GeneratedVisual"] = "GENVIS";
    })(exports.ApproachTypeName || (exports.ApproachTypeName = {}));
    /**
     * Flags indicating the approach fix type.
     * They can be combined to indicate multiple types on a single fix using BitFlags class.
     */
    exports.FixTypeFlags = void 0;
    (function (FixTypeFlags) {
        FixTypeFlags[FixTypeFlags["None"] = 0] = "None";
        /* Initial Approach Fix. */
        FixTypeFlags[FixTypeFlags["IAF"] = 1] = "IAF";
        /* Intermediate Fix. */
        FixTypeFlags[FixTypeFlags["IF"] = 2] = "IF";
        /* Missed Approach Point. */
        FixTypeFlags[FixTypeFlags["MAP"] = 4] = "MAP";
        /* Final Approach Fix. */
        FixTypeFlags[FixTypeFlags["FAF"] = 8] = "FAF";
        /* Missed Approach Holding Point. This doesn't come from the sim. We populate it in `insertApproach` in the Fms. */
        FixTypeFlags[FixTypeFlags["MAHP"] = 16] = "MAHP";
    })(exports.FixTypeFlags || (exports.FixTypeFlags = {}));
    /**
     * Flags indicating the rnav approach type.
     */
    exports.RnavTypeFlags = void 0;
    (function (RnavTypeFlags) {
        RnavTypeFlags[RnavTypeFlags["None"] = 0] = "None";
        RnavTypeFlags[RnavTypeFlags["LNAV"] = 1] = "LNAV";
        RnavTypeFlags[RnavTypeFlags["LNAVVNAV"] = 2] = "LNAVVNAV";
        RnavTypeFlags[RnavTypeFlags["LP"] = 4] = "LP";
        RnavTypeFlags[RnavTypeFlags["LPV"] = 8] = "LPV";
    })(exports.RnavTypeFlags || (exports.RnavTypeFlags = {}));
    /**
     * Bitflags describing types of data that can be loaded into an {@link AirportFacility} object.
     */
    exports.AirportFacilityDataFlags = void 0;
    (function (AirportFacilityDataFlags) {
        AirportFacilityDataFlags[AirportFacilityDataFlags["Minimal"] = 0] = "Minimal";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Approaches"] = 1] = "Approaches";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Departures"] = 2] = "Departures";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Arrivals"] = 4] = "Arrivals";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Frequencies"] = 8] = "Frequencies";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Gates"] = 16] = "Gates";
        AirportFacilityDataFlags[AirportFacilityDataFlags["HoldingPatterns"] = 32] = "HoldingPatterns";
        AirportFacilityDataFlags[AirportFacilityDataFlags["Runways"] = 64] = "Runways";
        AirportFacilityDataFlags[AirportFacilityDataFlags["All"] = 127] = "All";
    })(exports.AirportFacilityDataFlags || (exports.AirportFacilityDataFlags = {}));
    /**
     * The class of airport facility.
     */
    exports.AirportClass = void 0;
    (function (AirportClass) {
        /** No other airport class could be identified. */
        AirportClass[AirportClass["None"] = 0] = "None";
        /** The airport has at least one hard surface runway. */
        AirportClass[AirportClass["HardSurface"] = 1] = "HardSurface";
        /** The airport has no hard surface runways. */
        AirportClass[AirportClass["SoftSurface"] = 2] = "SoftSurface";
        /** The airport has only water surface runways. */
        AirportClass[AirportClass["AllWater"] = 3] = "AllWater";
        /** The airport has no runways, but does contain helipads. */
        AirportClass[AirportClass["HeliportOnly"] = 4] = "HeliportOnly";
        /** The airport is a non-public use airport. */
        AirportClass[AirportClass["Private"] = 5] = "Private";
    })(exports.AirportClass || (exports.AirportClass = {}));
    /**
     * The class of an airport facility, expressed as a mask for nearest airport search session filtering.
     */
    exports.AirportClassMask = void 0;
    (function (AirportClassMask) {
        /** No other airport class could be identified. */
        AirportClassMask[AirportClassMask["None"] = 0] = "None";
        /** The airport has at least one hard surface runway. */
        AirportClassMask[AirportClassMask["HardSurface"] = 2] = "HardSurface";
        /** The airport has no hard surface runways. */
        AirportClassMask[AirportClassMask["SoftSurface"] = 4] = "SoftSurface";
        /** The airport has only water surface runways. */
        AirportClassMask[AirportClassMask["AllWater"] = 8] = "AllWater";
        /** The airport has no runways, but does contain helipads. */
        AirportClassMask[AirportClassMask["HeliportOnly"] = 16] = "HeliportOnly";
        /** The airport is a non-public use airport. */
        AirportClassMask[AirportClassMask["Private"] = 32] = "Private";
    })(exports.AirportClassMask || (exports.AirportClassMask = {}));
    /**
     * An enumeration of possible intersection types.
     */
    exports.IntersectionType = void 0;
    (function (IntersectionType) {
        IntersectionType[IntersectionType["None"] = 0] = "None";
        IntersectionType[IntersectionType["Named"] = 1] = "Named";
        IntersectionType[IntersectionType["Unnamed"] = 2] = "Unnamed";
        IntersectionType[IntersectionType["Vor"] = 3] = "Vor";
        IntersectionType[IntersectionType["NDB"] = 4] = "NDB";
        IntersectionType[IntersectionType["Offroute"] = 5] = "Offroute";
        IntersectionType[IntersectionType["IAF"] = 6] = "IAF";
        IntersectionType[IntersectionType["FAF"] = 7] = "FAF";
        IntersectionType[IntersectionType["RNAV"] = 8] = "RNAV";
        IntersectionType[IntersectionType["VFR"] = 9] = "VFR";
    })(exports.IntersectionType || (exports.IntersectionType = {}));
    exports.TacanMode = void 0;
    (function (TacanMode) {
        TacanMode[TacanMode["None"] = 0] = "None";
        TacanMode[TacanMode["X"] = 88] = "X";
        TacanMode[TacanMode["Y"] = 89] = "Y";
    })(exports.TacanMode || (exports.TacanMode = {}));
    exports.UserFacilityType = void 0;
    (function (UserFacilityType) {
        UserFacilityType[UserFacilityType["RADIAL_RADIAL"] = 0] = "RADIAL_RADIAL";
        UserFacilityType[UserFacilityType["RADIAL_DISTANCE"] = 1] = "RADIAL_DISTANCE";
        UserFacilityType[UserFacilityType["LAT_LONG"] = 2] = "LAT_LONG";
    })(exports.UserFacilityType || (exports.UserFacilityType = {}));
    /**
     * ARINC 424 Leg Types
     */
    exports.LegType = void 0;
    (function (LegType) {
        /** An unknown leg type. */
        LegType[LegType["Unknown"] = 0] = "Unknown";
        /** An arc-to-fix leg. This indicates a DME arc leg to a specified fix.*/
        LegType[LegType["AF"] = 1] = "AF";
        /** A course-to-altitude leg. */
        LegType[LegType["CA"] = 2] = "CA";
        /**
         * A course-to-DME-distance leg. This leg is flown on a wind corrected course
         * to a specific DME distance from another fix.
         */
        LegType[LegType["CD"] = 3] = "CD";
        /** A course-to-fix leg.*/
        LegType[LegType["CF"] = 4] = "CF";
        /** A course-to-intercept leg. */
        LegType[LegType["CI"] = 5] = "CI";
        /** A course-to-radial intercept leg. */
        LegType[LegType["CR"] = 6] = "CR";
        /** A direct-to-fix leg, from an unspecified starting position. */
        LegType[LegType["DF"] = 7] = "DF";
        /**
         * A fix-to-altitude leg. A FA leg is flown on a track from a fix to a
         * specified altitude.
         */
        LegType[LegType["FA"] = 8] = "FA";
        /**
         * A fix-to-distance leg. This leg is flown on a track from a fix to a
         * specific distance from the fix.
         */
        LegType[LegType["FC"] = 9] = "FC";
        /**
         * A fix to DME distance leg. This leg is flown on a track from a fix to
         * a specific DME distance from another fix.
         */
        LegType[LegType["FD"] = 10] = "FD";
        /** A course-to-manual-termination leg. */
        LegType[LegType["FM"] = 11] = "FM";
        /** A hold-to-altitude leg. The hold is flown until a specified altitude is reached. */
        LegType[LegType["HA"] = 12] = "HA";
        /**
         * A hold-to-fix leg. This indicates one time around the hold circuit and
         * then an exit.
         */
        LegType[LegType["HF"] = 13] = "HF";
        /** A hold-to-manual-termination leg. */
        LegType[LegType["HM"] = 14] = "HM";
        /** Initial procedure fix. */
        LegType[LegType["IF"] = 15] = "IF";
        /** A procedure turn leg. */
        LegType[LegType["PI"] = 16] = "PI";
        /** A radius-to-fix leg, with endpoint fixes, a center fix, and a radius. */
        LegType[LegType["RF"] = 17] = "RF";
        /** A track-to-fix leg, from the previous fix to the terminator. */
        LegType[LegType["TF"] = 18] = "TF";
        /** A heading-to-altitude leg. */
        LegType[LegType["VA"] = 19] = "VA";
        /** A heading-to-DME-distance leg. */
        LegType[LegType["VD"] = 20] = "VD";
        /** A heading-to-intercept leg. */
        LegType[LegType["VI"] = 21] = "VI";
        /** A heading-to-manual-termination leg. */
        LegType[LegType["VM"] = 22] = "VM";
        /** A heading-to-radial intercept leg. */
        LegType[LegType["VR"] = 23] = "VR";
        /** A leg representing a lateral and vertical discontinuity in the flight plan. */
        LegType[LegType["Discontinuity"] = 99] = "Discontinuity";
        /** A leg representing a lateral and vertical discontinuity in the flight plan that does not prevent sequencing. */
        LegType[LegType["ThruDiscontinuity"] = 100] = "ThruDiscontinuity";
    })(exports.LegType || (exports.LegType = {}));
    /**
     * Types of altitude restrictions on procedure legs.
     */
    exports.AltitudeRestrictionType = void 0;
    (function (AltitudeRestrictionType) {
        AltitudeRestrictionType[AltitudeRestrictionType["Unused"] = 0] = "Unused";
        AltitudeRestrictionType[AltitudeRestrictionType["At"] = 1] = "At";
        AltitudeRestrictionType[AltitudeRestrictionType["AtOrAbove"] = 2] = "AtOrAbove";
        AltitudeRestrictionType[AltitudeRestrictionType["AtOrBelow"] = 3] = "AtOrBelow";
        AltitudeRestrictionType[AltitudeRestrictionType["Between"] = 4] = "Between";
    })(exports.AltitudeRestrictionType || (exports.AltitudeRestrictionType = {}));
    /**
     * Types of speed restrictions on procedure legs.
     * Note: {@link SpeedRestrictionType.Between} is not used in the nav database.
     */
    exports.SpeedRestrictionType = void 0;
    (function (SpeedRestrictionType) {
        SpeedRestrictionType[SpeedRestrictionType["Unused"] = 0] = "Unused";
        SpeedRestrictionType[SpeedRestrictionType["At"] = 1] = "At";
        SpeedRestrictionType[SpeedRestrictionType["AtOrAbove"] = 2] = "AtOrAbove";
        SpeedRestrictionType[SpeedRestrictionType["AtOrBelow"] = 3] = "AtOrBelow";
        SpeedRestrictionType[SpeedRestrictionType["Between"] = 4] = "Between";
    })(exports.SpeedRestrictionType || (exports.SpeedRestrictionType = {}));
    exports.LegTurnDirection = void 0;
    (function (LegTurnDirection) {
        LegTurnDirection[LegTurnDirection["None"] = 0] = "None";
        LegTurnDirection[LegTurnDirection["Left"] = 1] = "Left";
        LegTurnDirection[LegTurnDirection["Right"] = 2] = "Right";
        LegTurnDirection[LegTurnDirection["Either"] = 3] = "Either";
    })(exports.LegTurnDirection || (exports.LegTurnDirection = {}));
    exports.AirwayType = void 0;
    (function (AirwayType) {
        AirwayType[AirwayType["None"] = 0] = "None";
        AirwayType[AirwayType["Victor"] = 1] = "Victor";
        AirwayType[AirwayType["Jet"] = 2] = "Jet";
        AirwayType[AirwayType["Both"] = 3] = "Both";
    })(exports.AirwayType || (exports.AirwayType = {}));
    exports.NdbType = void 0;
    (function (NdbType) {
        NdbType[NdbType["CompassPoint"] = 0] = "CompassPoint";
        NdbType[NdbType["MH"] = 1] = "MH";
        NdbType[NdbType["H"] = 2] = "H";
        NdbType[NdbType["HH"] = 3] = "HH";
    })(exports.NdbType || (exports.NdbType = {}));
    exports.VorType = void 0;
    (function (VorType) {
        VorType[VorType["Unknown"] = 0] = "Unknown";
        VorType[VorType["VOR"] = 1] = "VOR";
        VorType[VorType["VORDME"] = 2] = "VORDME";
        VorType[VorType["DME"] = 3] = "DME";
        VorType[VorType["TACAN"] = 4] = "TACAN";
        VorType[VorType["VORTAC"] = 5] = "VORTAC";
        VorType[VorType["ILS"] = 6] = "ILS";
        VorType[VorType["VOT"] = 7] = "VOT";
    })(exports.VorType || (exports.VorType = {}));
    exports.RunwaySurfaceType = void 0;
    (function (RunwaySurfaceType) {
        RunwaySurfaceType[RunwaySurfaceType["Concrete"] = 0] = "Concrete";
        RunwaySurfaceType[RunwaySurfaceType["Grass"] = 1] = "Grass";
        RunwaySurfaceType[RunwaySurfaceType["WaterFSX"] = 2] = "WaterFSX";
        RunwaySurfaceType[RunwaySurfaceType["GrassBumpy"] = 3] = "GrassBumpy";
        RunwaySurfaceType[RunwaySurfaceType["Asphalt"] = 4] = "Asphalt";
        RunwaySurfaceType[RunwaySurfaceType["ShortGrass"] = 5] = "ShortGrass";
        RunwaySurfaceType[RunwaySurfaceType["LongGrass"] = 6] = "LongGrass";
        RunwaySurfaceType[RunwaySurfaceType["HardTurf"] = 7] = "HardTurf";
        RunwaySurfaceType[RunwaySurfaceType["Snow"] = 8] = "Snow";
        RunwaySurfaceType[RunwaySurfaceType["Ice"] = 9] = "Ice";
        RunwaySurfaceType[RunwaySurfaceType["Urban"] = 10] = "Urban";
        RunwaySurfaceType[RunwaySurfaceType["Forest"] = 11] = "Forest";
        RunwaySurfaceType[RunwaySurfaceType["Dirt"] = 12] = "Dirt";
        RunwaySurfaceType[RunwaySurfaceType["Coral"] = 13] = "Coral";
        RunwaySurfaceType[RunwaySurfaceType["Gravel"] = 14] = "Gravel";
        RunwaySurfaceType[RunwaySurfaceType["OilTreated"] = 15] = "OilTreated";
        RunwaySurfaceType[RunwaySurfaceType["SteelMats"] = 16] = "SteelMats";
        RunwaySurfaceType[RunwaySurfaceType["Bituminous"] = 17] = "Bituminous";
        RunwaySurfaceType[RunwaySurfaceType["Brick"] = 18] = "Brick";
        RunwaySurfaceType[RunwaySurfaceType["Macadam"] = 19] = "Macadam";
        RunwaySurfaceType[RunwaySurfaceType["Planks"] = 20] = "Planks";
        RunwaySurfaceType[RunwaySurfaceType["Sand"] = 21] = "Sand";
        RunwaySurfaceType[RunwaySurfaceType["Shale"] = 22] = "Shale";
        RunwaySurfaceType[RunwaySurfaceType["Tarmac"] = 23] = "Tarmac";
        RunwaySurfaceType[RunwaySurfaceType["WrightFlyerTrack"] = 24] = "WrightFlyerTrack";
        //SURFACE_TYPE_LAST_FSX
        RunwaySurfaceType[RunwaySurfaceType["Ocean"] = 26] = "Ocean";
        RunwaySurfaceType[RunwaySurfaceType["Water"] = 27] = "Water";
        RunwaySurfaceType[RunwaySurfaceType["Pond"] = 28] = "Pond";
        RunwaySurfaceType[RunwaySurfaceType["Lake"] = 29] = "Lake";
        RunwaySurfaceType[RunwaySurfaceType["River"] = 30] = "River";
        RunwaySurfaceType[RunwaySurfaceType["WasteWater"] = 31] = "WasteWater";
        RunwaySurfaceType[RunwaySurfaceType["Paint"] = 32] = "Paint";
        // UNUSED
        // SURFACE_TYPE_ERASE_GRASS
    })(exports.RunwaySurfaceType || (exports.RunwaySurfaceType = {}));
    exports.RunwayLightingType = void 0;
    (function (RunwayLightingType) {
        RunwayLightingType[RunwayLightingType["Unknown"] = 0] = "Unknown";
        RunwayLightingType[RunwayLightingType["None"] = 1] = "None";
        RunwayLightingType[RunwayLightingType["PartTime"] = 2] = "PartTime";
        RunwayLightingType[RunwayLightingType["FullTime"] = 3] = "FullTime";
        RunwayLightingType[RunwayLightingType["Frequency"] = 4] = "Frequency";
    })(exports.RunwayLightingType || (exports.RunwayLightingType = {}));
    exports.AirportPrivateType = void 0;
    (function (AirportPrivateType) {
        AirportPrivateType[AirportPrivateType["Uknown"] = 0] = "Uknown";
        AirportPrivateType[AirportPrivateType["Public"] = 1] = "Public";
        AirportPrivateType[AirportPrivateType["Military"] = 2] = "Military";
        AirportPrivateType[AirportPrivateType["Private"] = 3] = "Private";
    })(exports.AirportPrivateType || (exports.AirportPrivateType = {}));
    exports.GpsBoolean = void 0;
    (function (GpsBoolean) {
        GpsBoolean[GpsBoolean["Unknown"] = 0] = "Unknown";
        GpsBoolean[GpsBoolean["No"] = 1] = "No";
        GpsBoolean[GpsBoolean["Yes"] = 2] = "Yes";
    })(exports.GpsBoolean || (exports.GpsBoolean = {}));
    exports.VorClass = void 0;
    (function (VorClass) {
        VorClass[VorClass["Unknown"] = 0] = "Unknown";
        VorClass[VorClass["Terminal"] = 1] = "Terminal";
        VorClass[VorClass["LowAlt"] = 2] = "LowAlt";
        VorClass[VorClass["HighAlt"] = 3] = "HighAlt";
        VorClass[VorClass["ILS"] = 4] = "ILS";
        VorClass[VorClass["VOT"] = 5] = "VOT";
    })(exports.VorClass || (exports.VorClass = {}));
    exports.FacilitySearchType = void 0;
    (function (FacilitySearchType) {
        FacilitySearchType[FacilitySearchType["All"] = 0] = "All";
        FacilitySearchType[FacilitySearchType["Airport"] = 1] = "Airport";
        FacilitySearchType[FacilitySearchType["Intersection"] = 2] = "Intersection";
        FacilitySearchType[FacilitySearchType["Vor"] = 3] = "Vor";
        FacilitySearchType[FacilitySearchType["Ndb"] = 4] = "Ndb";
        FacilitySearchType[FacilitySearchType["Boundary"] = 5] = "Boundary";
        FacilitySearchType[FacilitySearchType["User"] = 6] = "User";
        FacilitySearchType[FacilitySearchType["Visual"] = 7] = "Visual";
        FacilitySearchType[FacilitySearchType["AllExceptVisual"] = 8] = "AllExceptVisual";
    })(exports.FacilitySearchType || (exports.FacilitySearchType = {}));
    /**
     * A type of airspace boundary.
     */
    exports.BoundaryType = void 0;
    (function (BoundaryType) {
        BoundaryType[BoundaryType["None"] = 0] = "None";
        BoundaryType[BoundaryType["Center"] = 1] = "Center";
        BoundaryType[BoundaryType["ClassA"] = 2] = "ClassA";
        BoundaryType[BoundaryType["ClassB"] = 3] = "ClassB";
        BoundaryType[BoundaryType["ClassC"] = 4] = "ClassC";
        BoundaryType[BoundaryType["ClassD"] = 5] = "ClassD";
        BoundaryType[BoundaryType["ClassE"] = 6] = "ClassE";
        BoundaryType[BoundaryType["ClassF"] = 7] = "ClassF";
        BoundaryType[BoundaryType["ClassG"] = 8] = "ClassG";
        BoundaryType[BoundaryType["Tower"] = 9] = "Tower";
        BoundaryType[BoundaryType["Clearance"] = 10] = "Clearance";
        BoundaryType[BoundaryType["Ground"] = 11] = "Ground";
        BoundaryType[BoundaryType["Departure"] = 12] = "Departure";
        BoundaryType[BoundaryType["Approach"] = 13] = "Approach";
        BoundaryType[BoundaryType["MOA"] = 14] = "MOA";
        BoundaryType[BoundaryType["Restricted"] = 15] = "Restricted";
        BoundaryType[BoundaryType["Prohibited"] = 16] = "Prohibited";
        BoundaryType[BoundaryType["Warning"] = 17] = "Warning";
        BoundaryType[BoundaryType["Alert"] = 18] = "Alert";
        BoundaryType[BoundaryType["Danger"] = 19] = "Danger";
        BoundaryType[BoundaryType["NationalPark"] = 20] = "NationalPark";
        BoundaryType[BoundaryType["ModeC"] = 21] = "ModeC";
        BoundaryType[BoundaryType["Radar"] = 22] = "Radar";
        BoundaryType[BoundaryType["Training"] = 23] = "Training";
    })(exports.BoundaryType || (exports.BoundaryType = {}));
    /**
     * A type of airspace boundary altitude maxima.
     */
    exports.BoundaryAltitudeType = void 0;
    (function (BoundaryAltitudeType) {
        BoundaryAltitudeType[BoundaryAltitudeType["Unknown"] = 0] = "Unknown";
        BoundaryAltitudeType[BoundaryAltitudeType["MSL"] = 1] = "MSL";
        BoundaryAltitudeType[BoundaryAltitudeType["AGL"] = 2] = "AGL";
        BoundaryAltitudeType[BoundaryAltitudeType["Unlimited"] = 3] = "Unlimited";
    })(exports.BoundaryAltitudeType || (exports.BoundaryAltitudeType = {}));
    /**
     * A type of boundary geometry vector.
     */
    exports.BoundaryVectorType = void 0;
    (function (BoundaryVectorType) {
        BoundaryVectorType[BoundaryVectorType["None"] = 0] = "None";
        BoundaryVectorType[BoundaryVectorType["Start"] = 1] = "Start";
        BoundaryVectorType[BoundaryVectorType["Line"] = 2] = "Line";
        BoundaryVectorType[BoundaryVectorType["Origin"] = 3] = "Origin";
        BoundaryVectorType[BoundaryVectorType["ArcCW"] = 4] = "ArcCW";
        BoundaryVectorType[BoundaryVectorType["ArcCCW"] = 5] = "ArcCCW";
        BoundaryVectorType[BoundaryVectorType["Circle"] = 6] = "Circle";
    })(exports.BoundaryVectorType || (exports.BoundaryVectorType = {}));

    /**
     * ICAO type strings.
     */
    exports.IcaoType = void 0;
    (function (IcaoType) {
        IcaoType["None"] = "";
        IcaoType["Airport"] = "A";
        IcaoType["Vor"] = "V";
        IcaoType["Ndb"] = "N";
        IcaoType["Waypoint"] = "W";
        IcaoType["Runway"] = "R";
        IcaoType["User"] = "U";
        IcaoType["VisualApproach"] = "S";
    })(exports.IcaoType || (exports.IcaoType = {}));

    /**
     * A utility class for working with ICAO values.
     */
    class ICAO {
        /**
         * Checks whether a query is an ICAO value object.
         * @param query A query.
         * @returns Whether the specified query is an ICAO value object.
         */
        static isValue(query) {
            return typeof query === 'object' && query !== null && query.__Type === 'JS_ICAO';
        }
        /**
         * Creates a new ICAO value.
         * @param type The ICAO's facility type.
         * @param region The ICAO's region code.
         * @param airport The ICAO's associated airport ident.
         * @param ident The ICAO's ident.
         * @returns A new ICAO value with the specified properties.
         */
        static value(type, region, airport, ident) {
            return {
                __Type: 'JS_ICAO',
                type,
                region,
                airport,
                ident,
            };
        }
        /**
         * Gets an empty ICAO value.
         * @returns An empty ICAO value.
         */
        static emptyValue() {
            return ICAO.EMPTY_ICAO_VALUE;
        }
        /**
         * Checks whether an ICAO value is empty.
         * @param value The value to check.
         * @returns Whether the specified ICAO value is empty.
         */
        static isValueEmpty(value) {
            return value.type === ''
                && value.region === ''
                && value.airport === ''
                && value.ident === '';
        }
        /**
         * Copies an ICAO value.
         * @param value The value to copy.
         * @returns A new copy of the specified ICAO value.
         */
        static copyValue(value) {
            return Object.assign({}, value);
        }
        /**
         * Converts an ICAO string (V1) to an ICAO value.
         * @param icao The ICAO string (V1) to convert.
         * @returns The ICAO value converted from the specified string (V1).
         */
        static stringV1ToValue(icao) {
            return ICAO.value(icao.substring(0, 1).trim(), icao.substring(1, 3).trim(), icao.substring(3, 7).trim(), ICAO.getIdentFromStringV1(icao));
        }
        /**
         * Converts an ICAO string (V2) to an ICAO value.
         * @param icao The ICAO string (V2) to convert.
         * @returns The ICAO value converted from the specified string (V2).
         */
        static stringV2ToValue(icao) {
            return ICAO.value(icao.substring(0, 1).trim(), icao.substring(1, 3).trim(), icao.substring(3, 11).trim(), ICAO.getIdentFromStringV2(icao));
        }
        /**
         * Converts an ICAO value to an ICAO string (V1).
         * @param icao The ICAO value to convert.
         * @returns The ICAO string (V1) converted from the specified value.
         * @throws Error if the ICAO value is malformed or cannot be converted to a string (V1).
         */
        static valueToStringV1(icao) {
            if (ICAO.isValueEmpty(icao)) {
                return ICAO.EMPTY_V1;
            }
            if (icao.type.length > 1) {
                throw new Error(`ICAO: cannot convert IcaoValue to V1 string - invalid type '${icao.type}'`);
            }
            if (icao.region.length > 2) {
                throw new Error(`ICAO: cannot convert IcaoValue to V1 string - invalid region '${icao.region}'`);
            }
            if (icao.airport.length > 4) {
                throw new Error(`ICAO: cannot convert IcaoValue to V1 string - invalid airport '${icao.airport}'`);
            }
            // Always drop the region from airport ICAOs for V1 strings to replicate sim behavior.
            return `${icao.type}${icao.type === exports.IcaoType.Airport ? '  ' : icao.region.padEnd(2, ' ')}${icao.airport.padEnd(4, ' ')}${icao.ident.padEnd(5, ' ')}`;
        }
        /**
         * Attempts to convert an ICAO value to an ICAO string (V1).
         * @param icao The ICAO value to convert.
         * @returns The ICAO string (V1) converted from the specified value, or the empty ICAO string (V1) if the ICAO value
         * is malformed or cannot be converted to a string (V1).
         */
        static tryValueToStringV1(icao) {
            if (ICAO.isValueEmpty(icao)
                || icao.type.length > 1
                || icao.region.length > 2
                || icao.airport.length > 4) {
                return ICAO.EMPTY_V1;
            }
            // Always drop the region from airport ICAOs for V1 strings to replicate sim behavior.
            return `${icao.type}${icao.type === exports.IcaoType.Airport ? '  ' : icao.region.padEnd(2, ' ')}${icao.airport.padEnd(4, ' ')}${icao.ident.padEnd(5, ' ')}`;
        }
        /**
         * Converts an ICAO value to an ICAO string (V2).
         * @param icao The ICAO value to convert.
         * @returns The ICAO string (V2) converted from the specified value.
         * @throws Error if the ICAO value is malformed or cannot be converted to a string (V2).
         */
        static valueToStringV2(icao) {
            if (ICAO.isValueEmpty(icao)) {
                return ICAO.EMPTY_V2;
            }
            if (icao.type.length > 1) {
                throw new Error(`ICAO: cannot convert IcaoValue to V2 string - invalid type '${icao.type}'`);
            }
            if (icao.region.length > 2) {
                throw new Error(`ICAO: cannot convert IcaoValue to V2 string - invalid region '${icao.region}'`);
            }
            if (icao.airport.length > 8) {
                throw new Error(`ICAO: cannot convert IcaoValue to V2 string - invalid airport '${icao.airport}'`);
            }
            return `${icao.type}${icao.region.padEnd(2, ' ')}${icao.airport.padEnd(8, ' ')}${icao.ident.padEnd(8, ' ')}`;
        }
        /**
         * Attempts to convert an ICAO value to an ICAO string (V2).
         * @param icao The ICAO value to convert.
         * @returns The ICAO string (V2) converted from the specified value, or the empty ICAO string (V2) if the ICAO value
         * is malformed or cannot be converted to a string (V2).
         */
        static tryValueToStringV2(icao) {
            if (ICAO.isValueEmpty(icao)
                || icao.type.length > 1
                || icao.region.length > 2
                || icao.airport.length > 8) {
                return ICAO.EMPTY_V2;
            }
            return `${icao.type}${icao.region.padEnd(2, ' ')}${icao.airport.padEnd(8, ' ')}${icao.ident.padEnd(8, ' ')}`;
        }
        /**
         * Checks whether two ICAO values are equal.
         * @param a The first value.
         * @param b The second value.
         * @returns Whether the two specified ICAO values are equal.
         */
        static valueEquals(a, b) {
            return a.type === b.type
                // Airport ICAOs with different region codes are still considered equal.
                && (a.type === exports.IcaoType.Airport || a.region === b.region)
                && a.airport === b.airport
                && a.ident === b.ident;
        }
        /**
         * Gets the region code defined by an ICAO string (V1).
         * @param icao An ICAO string (V1).
         * @returns The region code defined by the specified ICAO string (V1).
         */
        static getRegionCodeFromStringV1(icao) {
            return icao.substring(1, 3).trim();
        }
        /**
         * Gets the region code defined by an ICAO string (V2).
         * @param icao An ICAO string (V2).
         * @returns The region code defined by the specified ICAO string (V2).
         */
        static getRegionCodeFromStringV2(icao) {
            return icao.substring(1, 3).trim();
        }
        /**
         * Gets the ident of the associated airport defined by an ICAO string (V1).
         * @param icao An ICAO string (V1).
         * @returns The ident of the associated airport defined by the specified ICAO string (V1).
         */
        static getAirportIdentFromStringV1(icao) {
            return icao.substring(3, 7).trim();
        }
        /**
         * Gets the ident of the associated airport defined by an ICAO string (V2).
         * @param icao An ICAO string (V2).
         * @returns The ident of the associated airport defined by the specified ICAO string (V2).
         */
        static getAirportIdentFromStringV2(icao) {
            return icao.substring(3, 11).trim();
        }
        /**
         * Gets the ident string defined by an ICAO string (V1).
         * @param icao An ICAO string (V1).
         * @returns The ident string defined by the specified ICAO string (V1).
         */
        static getIdentFromStringV1(icao) {
            return icao.substring(7).trim();
        }
        /**
         * Gets the ident string defined by an ICAO string (V2).
         * @param icao An ICAO string (V2).
         * @returns The ident string defined by the specified ICAO string (V2).
         */
        static getIdentFromStringV2(icao) {
            return icao.substring(11).trim();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static isIcaoTypeFacility(icaoType, facilityType) {
            switch (icaoType) {
                case 'A':
                    return facilityType === undefined || facilityType === exports.FacilityType.Airport;
                case 'W':
                    return facilityType === undefined || facilityType === exports.FacilityType.Intersection;
                case 'V':
                    return facilityType === undefined || facilityType === exports.FacilityType.VOR;
                case 'N':
                    return facilityType === undefined || facilityType === exports.FacilityType.NDB;
                case 'U':
                    return facilityType === undefined || facilityType === exports.FacilityType.USR;
                case 'R':
                    return facilityType === undefined || facilityType === exports.FacilityType.RWY;
                case 'S':
                    return facilityType === undefined || facilityType === exports.FacilityType.VIS;
                default:
                    return false;
            }
        }
        /**
         * Checks whether an ICAO value defines a valid facility type (optionally of a specific type).
         * @param icao The ICAO value to check.
         * @param facilityType The facility type to check against. If not defined, then this method will return `true` as
         * long as the ICAO value defines any valid facility type.
         * @returns Whether the specified ICAO value defines a facility of the specified type.
         */
        static isValueFacility(icao, facilityType) {
            return ICAO.isIcaoTypeFacility(icao.type, facilityType);
        }
        /**
         * Checks whether an ICAO string (V1) defines a valid facility type (optionally of a specific type).
         * @param icao The ICAO string (V1) to check.
         * @param facilityType The facility type to check against. If not defined, then this method will return `true` as
         * long as the ICAO string defines any valid facility type.
         * @returns Whether the specified ICAO string (V1) defines a facility of the specified type.
         */
        static isStringV1Facility(icao, facilityType) {
            return ICAO.isIcaoTypeFacility(icao[0], facilityType);
        }
        /**
         * Checks whether an ICAO string (V2) defines a valid facility type (optionally of a specific type).
         * @param icao The ICAO string (V2) to check.
         * @param facilityType The facility type to check against. If not defined, then this method will return `true` as
         * long as the ICAO string defines any valid facility type.
         * @returns Whether the specified ICAO string (V2) defines a facility of the specified type.
         */
        static isStringV2Facility(icao, facilityType) {
            return ICAO.isIcaoTypeFacility(icao[0], facilityType);
        }
        /**
         * Gets the facility type defined by an ICAO facility type string.
         * @param icaoType An ICAO facility type string.
         * @returns The facility type defined by the specified ICAO facility type string.
         * @throws Error if the ICAO facility type string does not define a valid facility type.
         */
        static getFacilityTypeFromIcaoType(icaoType) {
            switch (icaoType) {
                case 'A':
                    return exports.FacilityType.Airport;
                case 'W':
                    return exports.FacilityType.Intersection;
                case 'V':
                    return exports.FacilityType.VOR;
                case 'N':
                    return exports.FacilityType.NDB;
                case 'U':
                    return exports.FacilityType.USR;
                case 'R':
                    return exports.FacilityType.RWY;
                case 'S':
                    return exports.FacilityType.VIS;
                default:
                    throw new Error(`ICAO: cannot convert ICAO type to facility type - unknown ICAO type: ${icaoType}`);
            }
        }
        /**
         * Gets the facility type defined by an ICAO value.
         * @param icao An ICAO value.
         * @returns The facility type defined by the specified ICAO value.
         * @throws Error if the ICAO value does not define a valid facility type.
         */
        static getFacilityTypeFromValue(icao) {
            return ICAO.getFacilityTypeFromIcaoType(icao.type);
        }
        /**
         * Gets the facility type defined by an ICAO string (V1).
         * @param icao An ICAO string (V1).
         * @returns The facility type defined by the specified ICAO string (V1).
         * @throws Error if the ICAO string does not define a valid facility type.
         */
        static getFacilityTypeFromStringV1(icao) {
            return ICAO.getFacilityTypeFromIcaoType(icao[0]);
        }
        /**
         * Gets the facility type defined by an ICAO string (V2).
         * @param icao An ICAO string (V2).
         * @returns The facility type defined by the specified ICAO string (V2).
         * @throws Error if the ICAO string does not define a valid facility type.
         */
        static getFacilityTypeFromStringV2(icao) {
            return ICAO.getFacilityTypeFromIcaoType(icao[0]);
        }
        /**
         * Gets a unique ID string for an ICAO value. For any two ICAO values, their unique ID strings are equal if and only
         * if the ICAO values are equal to each other.
         * @param icao The value for which to get a unique ID.
         * @returns A unique ID string for the specified ICAO value.
         */
        static getUid(icao) {
            let uid = icao.__cachedUid;
            if (uid !== undefined) {
                return uid;
            }
            if (ICAO.isValueEmpty(icao)) {
                uid = '';
            }
            else {
                // Drop the region from airport ICAOs because airport ICAOs with and without region codes are considered equal.
                uid = `${icao.type}\n${icao.type === exports.IcaoType.Airport ? '' : icao.region}\n${icao.airport}\n${icao.ident}`;
            }
            icao.__cachedUid = uid;
            return uid;
        }
    }
    /** An empty ICAO string (V1). */
    ICAO.EMPTY_V1 = '            ';
    /** An empty ICAO string (V2). */
    ICAO.EMPTY_V2 = '                   ';
    /**
     * An empty ICAO string (V1).
     * @deprecated Please use `ICAO.EMPTY_V1` instead.
     */
    ICAO.emptyIcao = ICAO.EMPTY_V1;
    ICAO.EMPTY_ICAO_VALUE = ICAO.value('', '', '', '');
    /**
     * Gets the region code defined by an ICAO string (V1).
     * @param icao An ICAO string (V1).
     * @returns The region code defined by the specified ICAO string (V1).
     * @deprecated Please use `ICAO.getRegionCodeFromStringV1()` instead.
     */
    ICAO.getRegionCode = ICAO.getRegionCodeFromStringV1;
    /**
     * Gets the ident of the associated airport defined by an ICAO string (V1).
     * @param icao An ICAO string (V1).
     * @returns The ident of the associated airport defined by the specified ICAO string (V1).
     * @deprecated Please use `ICAO.getAirportIdentFromStringV1()` instead.
     */
    ICAO.getAssociatedAirportIdent = ICAO.getAirportIdentFromStringV1;
    /**
     * Gets the ident string defined by an ICAO string (V1).
     * @param icao An ICAO string (V1).
     * @returns The ident string defined by the specified ICAO string (V1).
     * @deprecated Please use `ICAO.getIdentFromStringV1()` instead.
     */
    ICAO.getIdent = ICAO.getIdentFromStringV1;
    /**
     * Checks whether an ICAO string (V1 or V2) defines a valid facility type (optionally of a specific type).
     * @param icao The ICAO string to check.
     * @param facilityType The facility type to check against. If not defined, then this method will return `true` as
     * long as the ICAO value defines any valid facility type.
     * @returns Whether the specified ICAO string defines a facility of the specified type.
     * @deprecated Please use `ICAO.isStringV1Facility()` instead.
     */
    ICAO.isFacility = ICAO.isStringV1Facility;
    /**
     * Gets the facility type defined by an ICAO string (V1 or V2).
     * @param icao An ICAO string.
     * @returns The facility type defined by the specified ICAO string.
     * @throws Error if the ICAO string does not define a valid facility type.
     * @deprecated Please use `ICAO.getFacilityTypeFromStringV1()` instead.
     */
    ICAO.getFacilityType = ICAO.getFacilityTypeFromStringV1;

    /**
     * Utility functions for working with facilities.
     */
    class FacilityUtils {
        /**
         * Checks whether a facility is of a given type.
         * @param facility The facility to check.
         * @param type The facility type to check against.
         * @returns Whether the facility is of the specified type.
         */
        static isFacilityType(facility, type) {
            // Need to check for the intersection version of VOR/NDB facilities - these facilities have identical ICAOs
            // to their VOR/NDB counterparts, so we need to manually check the __Type property on the facility object.
            if (facility['__Type'] === 'JS_FacilityIntersection') {
                return type === exports.FacilityType.Intersection;
            }
            return ICAO.isFacility(facility.icao, type);
        }
        /**
         * Gets the magnetic variation at a facility, in degrees. If the facility is a VOR, the magnetic variation defined
         * by the VOR is returned. For all other facilities, the modeled magnetic variation at the location of the facility
         * is returned.
         * @param facility A facility.
         * @returns The magnetic variation at the specified facility, in degrees.
         */
        static getMagVar(facility) {
            if (FacilityUtils.isFacilityType(facility, exports.FacilityType.VOR)) {
                return -facility.magneticVariation; // VOR facility magvar is positive west instead of the standard positive east
            }
            else {
                return MagVar.get(facility.lat, facility.lon);
            }
        }
        /**
         * Gets latitude/longitude coordinates corresponding to a radial and distance from a reference facility.
         * @param reference The reference facility.
         * @param radial The magnetic radial, in degrees.
         * @param distance The distance, in nautical miles.
         * @param out The GeoPoint object to which to write the result.
         * @returns The latitude/longitude coordinates corresponding to the specified radial and distance from the reference
         * facility.
         */
        static getLatLonFromRadialDistance(reference, radial, distance, out) {
            return FacilityUtils.geoPointCache[0].set(reference).offset(MagVar.magneticToTrue(radial, FacilityUtils.getMagVar(reference)), UnitType.NMILE.convertTo(distance, UnitType.GA_RADIAN), out);
        }
        /**
         * Gets latitude/longitude coordinates corresponding to the intersection of two facility radials.
         * @param reference1 The first reference facility.
         * @param radial1 The first magnetic radial, in degrees.
         * @param reference2 The second reference facility.
         * @param radial2 The second magnetic radial, in degrees.
         * @param out The GeoPoint object to which to write the result.
         * @returns The latitude/longitude coordinates corresponding to the intersection of the two specified radials. If
         * the specified radials do not intersect at a unique point, `NaN` is written to both `lat` and `lon`.
         */
        static getLatLonFromRadialRadial(reference1, radial1, reference2, radial2, out) {
            const magVar1 = FacilityUtils.getMagVar(reference1);
            const magVar2 = FacilityUtils.getMagVar(reference2);
            const radialCircle1 = FacilityUtils.geoCircleCache[0].setAsGreatCircle(reference1, MagVar.magneticToTrue(radial1, magVar1));
            const radialCircle2 = FacilityUtils.geoCircleCache[1].setAsGreatCircle(reference2, MagVar.magneticToTrue(radial2, magVar2));
            const radial1IncludesRef2 = radialCircle1.includes(reference2);
            const radial2IncludesRef1 = radialCircle2.includes(reference1);
            if (radial1IncludesRef2 && radial2IncludesRef1) {
                // Radials are parallel or antiparallel, and therefore do not have a unique intersection point.
                return out.set(NaN, NaN);
            }
            else if (radial1IncludesRef2) {
                // Reference 2 lies along the great circle of radial 1. The intersection point therefore is either reference 2
                // or its antipode. One of the two lies on the radial, and the other lies on the anti-radial.
                return radialCircle1.angleAlong(reference1, reference2, Math.PI) < Math.PI ? out.set(reference2) : out.set(reference2).antipode();
            }
            else if (radial2IncludesRef1) {
                // Reference 1 lies along the great circle of radial 2. The intersection point therefore is either reference 1
                // or its antipode. One of the two lies on the radial, and the other lies on the anti-radial.
                return radialCircle2.angleAlong(reference2, reference1, Math.PI) < Math.PI ? out.set(reference1) : out.set(reference1).antipode();
            }
            // Radials, unlike great circles, do not circumscribe the globe. Therefore, we choose the order of the intersection
            // operation carefully to ensure that the first solution (if it exists) is the "correct" intersection.
            const numIntersections = radialCircle1.encircles(reference2)
                ? radialCircle2.intersectionGeoPoint(radialCircle1, FacilityUtils.intersectionCache)
                : radialCircle1.intersectionGeoPoint(radialCircle2, FacilityUtils.intersectionCache);
            if (numIntersections === 0) {
                return out.set(NaN, NaN);
            }
            return out.set(FacilityUtils.intersectionCache[0]);
        }
    }
    FacilityUtils.geoPointCache = [new GeoPoint(0, 0)];
    FacilityUtils.geoCircleCache = [new GeoCircle(Vec3Math.create(), 0), new GeoCircle(Vec3Math.create(), 0)];
    FacilityUtils.intersectionCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
    /**
     * Utility functions for working with intersection facilities.
     */
    class IntersectionFacilityUtils {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static isTerminal(arg) {
            const icao = typeof arg === 'string'
                ? ICAO.stringV1ToValue(arg)
                : ICAO.isValue(arg) ? arg : arg.icaoStruct;
            if (!ICAO.isValueFacility(icao, exports.FacilityType.Intersection)) {
                throw new Error(`Facility with ICAO ${ICAO.tryValueToStringV2(icao)} is not an intersection`);
            }
            return icao.airport.length > 0;
        }
        /**
         * Gets the non-terminal version of an intersection ICAO value. If the ICAO is already a non-terminal intersection
         * ICAO, then an identical ICAO will be returned.
         * @param icao An intersection ICAO.
         * @returns The non-terminal version of the specified intersection ICAO.
         * @throws Error if the specified ICAO is not an intersection ICAO.
         */
        static getNonTerminalIcaoValue(icao) {
            if (!ICAO.isValueFacility(icao, exports.FacilityType.Intersection)) {
                throw new Error(`Facility with ICAO ${ICAO.tryValueToStringV2(icao)} is not an intersection`);
            }
            if (icao.airport.length > 0) {
                return ICAO.value(icao.type, icao.region, '', icao.ident);
            }
            else {
                return icao;
            }
        }
        /**
         * Gets the non-terminal version of an intersection ICAO string (V1). If the ICAO is already a non-terminal
         * intersection ICAO, then an identical ICAO will be returned.
         * @param icao An intersection ICAO.
         * @returns The non-terminal version of the specified intersection ICAO.
         * @throws Error if the specified ICAO is not an intersection ICAO.
         * @deprecated Please use `getNonTerminalIcaoValue()` instead.
         */
        static getNonTerminalICAO(icao) {
            if (!ICAO.isFacility(icao, exports.FacilityType.Intersection)) {
                throw new Error(`Facility with ICAO ${icao} is not an intersection`);
            }
            return IntersectionFacilityUtils.TERMINAL_REGEX.test(icao) ? `${icao.substring(0, 3)}    ${icao.substring(7)}` : icao;
        }
        /**
         * Gets an ICAO string from itself.
         * @param icao An ICAO string.
         * @returns The specified ICAO string.
         */
        static getIcaoIdentity(icao) {
            return icao;
        }
        /**
         * Gets an ICAO string from a facility.
         * @param facility A facility.
         * @returns The specified facility's ICAO string.
         */
        static getIcaoFacility(facility) {
            return facility.icaoStruct;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static filterDuplicates(array, arg2, arg3) {
            if (array.length === 0) {
                return [];
            }
            let getIcao;
            let retainTerminal;
            if (typeof arg2 === 'function') {
                getIcao = arg2;
                retainTerminal = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                retainTerminal = arg2 !== null && arg2 !== void 0 ? arg2 : false;
                if (ICAO.isValue(array[0])) {
                    getIcao = IntersectionFacilityUtils.getIcaoIdentity;
                }
                else {
                    getIcao = IntersectionFacilityUtils.getIcaoFacility;
                }
            }
            // Build the set of ICAOs to filter.
            IntersectionFacilityUtils.filterDuplicatesSet.clear();
            for (let i = 0; i < array.length; i++) {
                const icao = getIcao(array[i]);
                if (ICAO.isValueFacility(icao, exports.FacilityType.Intersection) && IntersectionFacilityUtils.isTerminal(icao) === retainTerminal) {
                    IntersectionFacilityUtils.filterDuplicatesSet.add(ICAO.getUid(IntersectionFacilityUtils.getNonTerminalIcaoValue(icao)));
                }
            }
            // If there are no ICAOs to filter, then just return a copy of the original array.
            if (IntersectionFacilityUtils.filterDuplicatesSet.size === 0) {
                return array.slice();
            }
            const filtered = array.filter(icao => {
                return IntersectionFacilityUtils.filterDuplicatesHelper(icao, getIcao, retainTerminal, IntersectionFacilityUtils.filterDuplicatesSet);
            });
            IntersectionFacilityUtils.filterDuplicatesSet.clear();
            return filtered;
        }
        /**
         * Checks whether an element should be filtered out from an array such that the filtered array does not contain any
         * elements that are mapped to duplicate terminal/non-terminal intersection pairs.
         * @param element The element to check.
         * @param getIcao A function which maps elements to ICAOs.
         * @param retainTerminal Whether to retain the terminal version of a duplicate pair in the filtered array.
         * @param nonTerminalIcaosToFilter A set of non-terminal ICAOs to filter out of the array.
         * @returns Whether the specified element should be filtered out from an array such that the filtered array does not
         * contain any elements that are mapped to duplicate terminal/non-terminal intersection pairs.
         */
        static filterDuplicatesHelper(element, getIcao, retainTerminal, nonTerminalIcaosToFilter) {
            const icao = getIcao(element);
            if (!ICAO.isValueFacility(icao, exports.FacilityType.Intersection)) {
                return true;
            }
            const isTerminal = IntersectionFacilityUtils.isTerminal(icao);
            if (isTerminal === retainTerminal) {
                return true;
            }
            if (isTerminal) {
                return !nonTerminalIcaosToFilter.has(ICAO.getUid(IntersectionFacilityUtils.getNonTerminalIcaoValue(icao)));
            }
            else {
                return !nonTerminalIcaosToFilter.has(ICAO.getUid(icao));
            }
        }
    }
    IntersectionFacilityUtils.TERMINAL_REGEX = /^...[a-zA-Z\d]/;
    IntersectionFacilityUtils.filterDuplicatesSet = new Set();
    /**
     * Utility functions for working with user facilities.
     */
    class UserFacilityUtils {
        /**
         * Creates a user facility from latitude/longitude coordinates.
         * @param icao The ICAO string of the new facility.
         * @param lat The latitude of the new facility.
         * @param lon The longitude of the new facility.
         * @param isTemporary Whether the new facility is temporary.
         * @param name The name of the new facility.
         * @returns A new user facility.
         */
        static createFromLatLon(icao, lat, lon, isTemporary = false, name) {
            const fac = {
                icao,
                icaoStruct: ICAO.stringV1ToValue(icao),
                name: name !== null && name !== void 0 ? name : '',
                lat,
                lon,
                userFacilityType: exports.UserFacilityType.LAT_LONG,
                isTemporary,
                region: '',
                city: '',
            };
            return fac;
        }
        /**
         * Creates a user facility from a radial and distance relative to a reference facility.
         * @param icao The ICAO string of the new facility.
         * @param reference The reference facility.
         * @param radial The magnetic radial, in degrees, of the reference facility on which the new facility lies.
         * @param distance The distance, in nautical miles, from the reference facility.
         * @param isTemporary Whether the new facility is temporary.
         * @param name The name of the new facility.
         * @returns A new user facility.
         */
        static createFromRadialDistance(icao, reference, radial, distance, isTemporary = false, name) {
            const location = FacilityUtils.getLatLonFromRadialDistance(reference, radial, distance, UserFacilityUtils.geoPointCache[0]);
            return {
                icao,
                icaoStruct: ICAO.stringV1ToValue(icao),
                name: name !== null && name !== void 0 ? name : '',
                lat: location.lat,
                lon: location.lon,
                userFacilityType: exports.UserFacilityType.RADIAL_DISTANCE,
                isTemporary,
                region: '',
                city: '',
                reference1IcaoStruct: ICAO.copyValue(reference.icaoStruct),
                reference1Icao: reference.icao,
                reference1Radial: radial,
                reference1MagVar: FacilityUtils.getMagVar(reference),
                reference1Distance: distance
            };
        }
        /**
         * Creates a user facility from a radial and distance relative to a reference facility.
         * @param icao The ICAO string of the new facility.
         * @param reference1 The first reference facility.
         * @param radial1 The magnetic radial, in degrees, of the first reference facility on which the new facility lies.
         * @param reference2 The second reference facility.
         * @param radial2 The magnetic radial, in degrees, of the second reference facility on which the new facility lies.
         * @param isTemporary Whether the new facility is temporary.
         * @param name The name of the new facility.
         * @returns A new user facility, or `undefined` if the specified radials do not intersect at a unique point.
         */
        static createFromRadialRadial(icao, reference1, radial1, reference2, radial2, isTemporary = false, name) {
            const location = FacilityUtils.getLatLonFromRadialRadial(reference1, radial1, reference2, radial2, UserFacilityUtils.geoPointCache[0]);
            if (isNaN(location.lat) || isNaN(location.lon)) {
                return undefined;
            }
            return {
                icao,
                icaoStruct: ICAO.stringV1ToValue(icao),
                name: name !== null && name !== void 0 ? name : '',
                lat: location.lat,
                lon: location.lon,
                userFacilityType: exports.UserFacilityType.RADIAL_RADIAL,
                isTemporary,
                region: '',
                city: '',
                reference1IcaoStruct: ICAO.copyValue(reference1.icaoStruct),
                reference1Icao: reference1.icao,
                reference1Radial: radial1,
                reference1MagVar: FacilityUtils.getMagVar(reference1),
                reference2IcaoStruct: ICAO.copyValue(reference2.icaoStruct),
                reference2Icao: reference2.icao,
                reference2Radial: radial2,
                reference2MagVar: FacilityUtils.getMagVar(reference2)
            };
        }
    }
    UserFacilityUtils.geoPointCache = [new GeoPoint(0, 0)];
    /**
     * Utilities to deal with TACAN facilities.
     */
    class TacanUtils {
        /**
         * Converts a VOR frequency to a TACAN channel.
         * @param frequency The frequency of the VOR.
         * @returns The TACAN channel.
         */
        static frequencyToChannel(frequency) {
            const uFrequency = frequency * 10;
            let res = 0;
            if (uFrequency <= 1122) {
                //108.0 to 112.25
                res = (uFrequency - 1063) % 256; //Protect against overflow
            }
            else if (uFrequency <= 1179) {
                res = (uFrequency - 1053) % 256;
            }
            else if (uFrequency < 1333) {
                return '';
            }
            else if (uFrequency <= 1342) {
                res = (uFrequency - 1273) % 256;
            }
            else {
                res = (uFrequency - 1343) % 256;
            }
            const letter = (Math.round(frequency * 100) % 10) === 0 ? 'X' : 'Y';
            return res.toFixed(0) + letter;
        }
    }

    /**
     * A Subject which provides a {@link GeoPointInterface} value.
     */
    class GeoPointSubject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         * @param tolerance The tolerance of this subject's equality check, defined as the maximum allowed great-circle
         * distance between two equal points in great-arc radians. Defaults to {@link GeoPoint.EQUALITY_TOLERANCE}.
         */
        constructor(value, tolerance) {
            super();
            this.value = value;
            this.tolerance = tolerance;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a GeoPointSubject.
         * @param initialVal The initial value.
         * @param tolerance The tolerance of the subject's equality check, defined as the maximum allowed great-circle
         * distance between two equal points in great-arc radians. Defaults to {@link GeoPoint.EQUALITY_TOLERANCE}.
         * @returns A GeoPointSubject.
         */
        static create(initialVal, tolerance) {
            return new GeoPointSubject(initialVal, tolerance);
        }
        /**
         * Creates a GeoPointSubject.
         * @param initialVal The initial value.
         * @returns A GeoPointSubject.
         * @deprecated Use `GeoPointSubject.create()` instead.
         */
        static createFromGeoPoint(initialVal) {
            return new GeoPointSubject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value.readonly;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2) {
            const isArg1Number = typeof arg1 === 'number';
            const equals = isArg1Number ? this.value.equals(arg1, arg2, this.tolerance) : this.value.equals(arg1, this.tolerance);
            if (!equals) {
                isArg1Number ? this.value.set(arg1, arg2) : this.value.set(arg1);
                this.notify();
            }
        }
    }

    /**
     * A partial implementation of a MutableGeoProjection. Subclasses should use the projectRaw() and invertRaw() methods
     * to define the type of projection to be implemented.
     */
    class AbstractGeoProjection {
        constructor() {
            this.center = new GeoPoint(0, 0);
            this.centerTranslation = new Float64Array(2);
            this.scaleFactor = UnitType.GA_RADIAN.convertTo(1, UnitType.NMILE); // 1 pixel = 1 nautical mile
            this.preRotation = new Float64Array(3);
            this.translation = new Float64Array(2);
            this.postRotation = 0;
            this.rotationSin = 0;
            this.rotationCos = 1;
            this.reflectY = 1;
            this.preRotationForwardTransform = new Transform3D();
            this.preRotationReverseTransform = new Transform3D();
            this.rotationCache = [new Transform3D(), new Transform3D()];
        }
        /** @inheritdoc */
        getCenter() {
            return this.center.readonly;
        }
        /** @inheritdoc */
        getScaleFactor() {
            return this.scaleFactor;
        }
        /** @inheritdoc */
        getPreRotation() {
            return this.preRotation;
        }
        /** @inheritdoc */
        getTranslation() {
            return this.translation;
        }
        /** @inheritdoc */
        getPostRotation() {
            return this.postRotation;
        }
        /** @inheritdoc */
        getReflectY() {
            return this.reflectY === -1;
        }
        /** @inheritdoc */
        setCenter(point) {
            this.center.set(point);
            this.updateCenterTranslation();
            return this;
        }
        /** @inheritdoc */
        setScaleFactor(factor) {
            this.scaleFactor = factor;
            return this;
        }
        /** @inheritdoc */
        setPreRotation(vec) {
            this.preRotation.set(vec);
            this.updatePreRotationTransforms();
            this.updateCenterTranslation();
            return this;
        }
        /** @inheritdoc */
        setTranslation(vec) {
            this.translation.set(vec);
            return this;
        }
        /** @inheritdoc */
        setPostRotation(rotation) {
            this.postRotation = rotation;
            this.rotationCos = Math.cos(rotation);
            this.rotationSin = Math.sin(rotation);
            return this;
        }
        /** @inheritdoc */
        setReflectY(val) {
            this.reflectY = val ? -1 : 1;
            return this;
        }
        /** @inheritdoc */
        copyParametersFrom(other) {
            return this.setCenter(other.getCenter())
                .setPreRotation(other.getPreRotation())
                .setScaleFactor(other.getScaleFactor())
                .setTranslation(other.getTranslation())
                .setPostRotation(other.getPostRotation())
                .setReflectY(other.getReflectY());
        }
        /**
         * Updates the pre-rotation transformation matrices.
         */
        updatePreRotationTransforms() {
            const phi = this.preRotation[1];
            const gamma = this.preRotation[2];
            this.rotationCache[0].toRotationX(gamma);
            this.rotationCache[1].toRotationY(-phi);
            Transform3D.concat(this.preRotationForwardTransform, this.rotationCache);
            this.preRotationReverseTransform.set(this.preRotationForwardTransform);
            this.preRotationReverseTransform.invert();
        }
        /**
         * Updates the translation vector to move the center of this projection to the origin.
         */
        updateCenterTranslation() {
            const centerArray = AbstractGeoProjection.vec2Cache[0];
            centerArray[0] = this.center.lon;
            centerArray[1] = this.center.lat;
            this.preRotateForward(centerArray, centerArray);
            this.projectRaw(centerArray, this.centerTranslation);
        }
        /**
         * Applies a forward rotation to a set of lat/lon coordinates using this projection's pre-projection rotation angles.
         * @param vec - the lat/lon coordinates to rotate, as a vector ([long, lat]).
         * @param out - the vector to which to write the result.
         * @returns the rotated lat/lon coordinates.
         */
        preRotateForward(vec, out) {
            const lambda = this.preRotation[0];
            const phi = this.preRotation[1];
            const gamma = this.preRotation[2];
            if (lambda === 0 && phi === 0 && gamma === 0) {
                out.set(vec);
                return out;
            }
            const lat = vec[1];
            const lon = vec[0];
            const rotatedLon = ((lon + lambda * Avionics.Utils.RAD2DEG) % 360 + 540) % 360 - 180; // enforce [-180, 180)
            if (phi === 0 && gamma === 0) {
                return Vec2Math.set(rotatedLon, lat, out);
            }
            const cartesianVec = GeoPoint.sphericalToCartesian(lat, rotatedLon, AbstractGeoProjection.vec3Cache[0]);
            const rotatedCartesianVec = this.preRotationForwardTransform.apply(cartesianVec, cartesianVec);
            const rotated = AbstractGeoProjection.geoPointCache[0].setFromCartesian(rotatedCartesianVec);
            return Vec2Math.set(rotated.lon, rotated.lat, out);
        }
        /**
         * Applies a reverse rotation to a set of lat/lon coordinates using this projection's pre-projection rotation angles.
         * @param vec - the lat/lon coordinates to rotate, as a vector ([long, lat]).
         * @param out - the vector to which to write the result.
         * @returns the rotated lat/lon coordinates.
         */
        preRotateReverse(vec, out) {
            const lambda = this.preRotation[0];
            const phi = this.preRotation[1];
            const gamma = this.preRotation[2];
            if (lambda === 0 && phi === 0 && gamma === 0) {
                out.set(vec);
                return out;
            }
            const lat = vec[1];
            const lon = vec[0];
            let rotatedLat = lat;
            let rotatedLon = lon;
            if (phi !== 0 || gamma !== 0) {
                const rotatedCartesianVec = GeoPoint.sphericalToCartesian(rotatedLat, rotatedLon, AbstractGeoProjection.vec3Cache[0]);
                const cartesianVec = this.preRotationReverseTransform.apply(rotatedCartesianVec, rotatedCartesianVec);
                const unrotated = AbstractGeoProjection.geoPointCache[0].setFromCartesian(cartesianVec);
                rotatedLat = unrotated.lat;
                rotatedLon = unrotated.lon;
            }
            rotatedLon = ((rotatedLon - lambda * Avionics.Utils.RAD2DEG) % 360 + 540) % 360 - 180; // enforce [-180, 180)
            return Vec2Math.set(rotatedLon, rotatedLat, out);
        }
        /** @inheritdoc */
        project(point, out) {
            if (point instanceof Float64Array) {
                out.set(point);
            }
            else {
                out[0] = point.lon;
                out[1] = point.lat;
            }
            this.preRotateForward(out, out);
            this.projectRaw(out, out);
            // translate projected center point to origin
            out[0] -= this.centerTranslation[0];
            out[1] -= this.centerTranslation[1];
            // apply y-reflection
            out[1] *= this.reflectY;
            // apply scale factor
            out[0] *= this.scaleFactor;
            out[1] *= this.scaleFactor;
            // apply post-projection rotation
            const x = out[0];
            const y = out[1];
            out[0] = x * this.rotationCos - y * this.rotationSin;
            out[1] = x * this.rotationSin + y * this.rotationCos;
            // apply post-projection translation
            out[0] += this.translation[0];
            out[1] += this.translation[1];
            return out;
        }
        /** @inheritdoc */
        invert(vec, out) {
            const projected = AbstractGeoProjection.vec2Cache[0];
            projected.set(vec);
            // invert post-projection translation
            projected[0] -= this.translation[0];
            projected[1] -= this.translation[1];
            // invert post-projection rotation
            const x = projected[0];
            const y = projected[1];
            projected[0] = x * this.rotationCos + y * this.rotationSin;
            projected[1] = -x * this.rotationSin + y * this.rotationCos;
            // invert scale factor
            projected[0] /= this.scaleFactor;
            projected[1] /= this.scaleFactor;
            // invert y-reflection
            projected[1] *= this.reflectY;
            // translate projected center point to default projected position
            projected[0] += this.centerTranslation[0];
            projected[1] += this.centerTranslation[1];
            const inverted = this.invertRaw(projected, projected);
            this.preRotateReverse(inverted, inverted);
            if (out instanceof Float64Array) {
                out.set(inverted);
                return out;
            }
            else {
                return out.set(inverted[1], inverted[0]);
            }
        }
    }
    AbstractGeoProjection.vec2Cache = [new Float64Array(2)];
    AbstractGeoProjection.vec3Cache = [new Float64Array(3)];
    AbstractGeoProjection.geoPointCache = [new GeoPoint(0, 0)];
    /**
     * A Mercator projection.
     */
    class MercatorProjection extends AbstractGeoProjection {
        /**
         * Applies a raw projection.
         * @param vec - a [lon, lat] vector describing the geographic point to project.
         * @param out - a 2D vector to which to write the result.
         * @returns the projected point.
         */
        projectRaw(vec, out) {
            out[0] = vec[0] * Avionics.Utils.DEG2RAD;
            out[1] = Math.log(Math.tan((90 + vec[1]) * Avionics.Utils.DEG2RAD / 2));
            return out;
        }
        /**
         * Inverts a raw projection.
         * @param vec - a 2D vector describing the projected point to invert.
         * @param out - a 2D vector to which to write the result.
         * @returns the inverted point.
         */
        invertRaw(vec, out) {
            out[0] = vec[0] * Avionics.Utils.RAD2DEG;
            out[1] = 2 * Math.atan(Math.exp(vec[1])) * Avionics.Utils.RAD2DEG - 90;
            return out;
        }
    }
    /**
     * An orthographic projection.
     */
    class OrthographicProjection extends AbstractGeoProjection {
        /**
         * Applies a raw projection.
         * @param vec - a [lon, lat] vector describing the geographic point to project.
         * @param out - a 2D vector to which to write the result.
         * @returns the projected point.
         */
        projectRaw(vec, out) {
            const lonRad = vec[0] * Avionics.Utils.DEG2RAD;
            const latRad = vec[1] * Avionics.Utils.DEG2RAD;
            out[0] = Math.cos(latRad) * Math.sin(lonRad);
            out[1] = Math.sin(latRad);
            return out;
        }
        /**
         * Inverts a raw projection.
         * @param vec - a 2D vector describing the projected point to invert.
         * @param out - a 2D vector to which to write the result.
         * @returns the inverted point.
         */
        invertRaw(vec, out) {
            const x = vec[0];
            const y = vec[1];
            const rho = Math.hypot(x, y);
            const c = Math.asin(rho);
            const sinC = Math.sin(c);
            const cosC = Math.cos(c);
            out[0] = Math.atan2(x * sinC, rho * cosC) * Avionics.Utils.RAD2DEG;
            out[1] = Math.asin(rho === 0 ? rho : y * sinC / rho) * Avionics.Utils.RAD2DEG;
            return out;
        }
    }

    /**
     * Resamples projected geodesic (great-circle) paths between defined endpoints into series of straight line segments.
     */
    class GeodesicResampler {
        /**
         * Constructor.
         * @param minDistance The minimum geodesic distance this resampler enforces between two adjacent resampled points, in
         * great-arc radians.
         * @param dpTolerance The Douglas-Peucker tolerance this resampler uses when deciding whether to discard a resampled
         * point during the line simplification process.
         * @param maxDepth The maximum depth of the resampling algorithm used by this resampler. The number of resampled
         * points is bounded from above by 2^[maxDepth] - 1.
         */
        constructor(minDistance, dpTolerance, maxDepth) {
            this.minDistance = minDistance;
            this.dpTolerance = dpTolerance;
            this.maxDepth = maxDepth;
            this.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0), new GeoPoint(0, 0)];
            this.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2)];
            this.vec3Cache = [new Float64Array(3), new Float64Array(3), new Float64Array(3)];
            this.cosMinDistance = Math.cos(minDistance);
            this.dpTolSq = dpTolerance * dpTolerance;
        }
        /**
         * Resamples a projected geodesic (great-circle) path.
         * @param projection The projection to use.
         * @param start The start of the path.
         * @param end The end of the path.
         * @param handler A function to handle the resampled points. The function is called once for each resampled point,
         * in order.
         * @throws Error when the start and end of the path are antipodal.
         */
        resample(projection, start, end, handler) {
            const startVec = GeoPoint.sphericalToCartesian(start, this.vec3Cache[0]);
            const endVec = GeoPoint.sphericalToCartesian(end, this.vec3Cache[1]);
            const startProjected = projection.project(start, this.vec2Cache[0]);
            const endProjected = projection.project(end, this.vec2Cache[1]);
            handler(this.geoPointCache[0].set(start), startProjected, 0);
            const index = this.resampleHelper(projection, start.lat, start.lon, startVec[0], startVec[1], startVec[2], startProjected[0], startProjected[1], end.lat, end.lon, endVec[0], endVec[1], endVec[2], endProjected[0], endProjected[1], handler, 0, 1);
            handler(this.geoPointCache[0].set(end), endProjected, index);
        }
        /**
         * Resamples a projected geodesic (great-circle) path. This method will recursively split the path into two halves
         * and resample the midpoint. Recursion continues as long as the maximum depth has not been reached and at least one
         * of the following conditions is met:
         * * The distance from the midpoint to the endpoints is greater than or equal to the minimum resampling distance.
         * * The Douglas-Peucker metric of the projected midpoint is greater than or equal to the set tolerance.
         * @param projection The projection to use.
         * @param lat1 The latitude of the start of the path.
         * @param lon1 The longitude of the start of the path.
         * @param x1 The x-component of the Cartesian position vector of the start of the path.
         * @param y1 The y-component of the Cartesian position vector of the start of the path.
         * @param z1 The z-component of the Cartesian position vector of the start of the path.
         * @param projX1 The x-component of the projected location of the start of the path.
         * @param projY1 The y-component of the projected location of the start of the path.
         * @param lat2 The latitude of the end of the path.
         * @param lon2 The longitude of the end of the path.
         * @param x2 The x-component of the Cartesian position vector of the end of the path.
         * @param y2 The y-component of the Cartesian position vector of the end of the path.
         * @param z2 The z-component of the Cartesian position vector of the end of the path.
         * @param projX2 The x-component of the projected location of the end of the path.
         * @param projY2 The y-component of the projected location of the end of the path.
         * @param handler A function to handle the resampled points.
         * @param depth The current depth of the resampling algorithm.
         * @param index The index of the next resampled point.
         * @returns The index of the next resampled point.
         * @throws Error when the start and end of the path are antipodal.
         */
        resampleHelper(projection, lat1, lon1, x1, y1, z1, projX1, projY1, lat2, lon2, x2, y2, z2, projX2, projY2, handler, depth, index) {
            if (depth >= this.maxDepth) {
                return index;
            }
            const startVec = Vec3Math.set(x1, y1, z1, this.vec3Cache[0]);
            const endVec = Vec3Math.set(x2, y2, z2, this.vec3Cache[1]);
            const sumVec = Vec3Math.add(startVec, endVec, this.vec3Cache[2]);
            if (Vec3Math.dot(sumVec, sumVec) === 0) {
                throw new Error('Cannot resample from antipodal endpoints.');
            }
            const midVec = Vec3Math.normalize(sumVec, sumVec);
            const startProjected = Vec2Math.set(projX1, projY1, this.vec2Cache[0]);
            const endProjected = Vec2Math.set(projX2, projY2, this.vec2Cache[1]);
            const deltaProjected = Vec2Math.sub(endProjected, startProjected, this.vec2Cache[2]);
            const deltaProjectedDot = Vec2Math.dot(deltaProjected, deltaProjected);
            const midPoint = this.geoPointCache[0].setFromCartesian(midVec);
            const midProjected = projection.project(midPoint, this.vec2Cache[2]);
            const lat0 = midPoint.lat;
            const lon0 = midPoint.lon;
            const x0 = midVec[0];
            const y0 = midVec[1];
            const z0 = midVec[2];
            const projX0 = midProjected[0];
            const projY0 = midProjected[1];
            const cosDistance = Vec3Math.dot(startVec, midVec);
            // cosine of distance increases with decreasing distance, so the check needs to be greater than.
            if (cosDistance > this.cosMinDistance) {
                // calculate Douglas-Peucker metric.
                const area = (projX2 - projX1) * (projY1 - projY0) - (projX1 - projX0) * (projY2 - projY1);
                const dpDisSq = area * area / deltaProjectedDot;
                if (dpDisSq < this.dpTolSq) {
                    return index;
                }
            }
            index = this.resampleHelper(projection, lat1, lon1, x1, y1, z1, projX1, projY1, lat0, lon0, x0, y0, z0, projX0, projY0, handler, depth + 1, index);
            handler(this.geoPointCache[0].set(lat0, lon0), Vec2Math.set(projX0, projY0, this.vec2Cache[0]), index);
            return this.resampleHelper(projection, lat0, lon0, x0, y0, z0, projX0, projY0, lat2, lon2, x2, y2, z2, projX2, projY2, handler, depth + 1, index + 1);
        }
    }

    /**
     * Resamples projected great- and small-circle paths between defined endpoints into series of straight line segments and circular arcs.
     */
    class GeoCircleResampler {
        /**
         * Constructor.
         * @param minDistance The minimum great-circle distance this resampler enforces between two adjacent resampled
         * points, in great-arc radians.
         * @param dpTolerance The Douglas-Peucker tolerance, in pixels, this resampler uses when deciding whether to discard
         * a resampled point during the simplification process.
         * @param maxDepth The maximum depth of the resampling algorithm used by this resampler. The number of resampled
         * points is bounded from above by `2^[maxDepth] - 1`.
         */
        constructor(minDistance, dpTolerance, maxDepth) {
            this.minDistance = minDistance;
            this.dpTolerance = dpTolerance;
            this.maxDepth = maxDepth;
            this.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
            this.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2)];
            this.vec3Cache = [new Float64Array(3), new Float64Array(3), new Float64Array(3), new Float64Array(3), new Float64Array(3)];
            this.startVector = {
                type: 'start',
                point: new GeoPoint(0, 0),
                projected: new Float64Array(2),
                index: 0
            };
            this.lineVector = {
                type: 'line',
                point: new GeoPoint(0, 0),
                projected: new Float64Array(2),
                index: 0
            };
            this.arcVector = {
                type: 'arc',
                point: new GeoPoint(0, 0),
                projected: new Float64Array(2),
                projectedArcCenter: new Float64Array(2),
                projectedArcRadius: 0,
                projectedArcStartAngle: 0,
                projectedArcEndAngle: 0,
                index: 0
            };
            this.state = {
                index: 0,
                prevX: 0,
                prevY: 0,
                vectorType: 'line',
                arcCenterX: 0,
                arcCenterY: 0,
                arcRadius: 0,
                isArcCounterClockwise: false
            };
            this.cosMinDistance = Math.cos(minDistance);
            this.dpTolSq = dpTolerance * dpTolerance;
        }
        /**
         * Resamples a projected great- or small-circle path.
         * @param projection The projection to use.
         * @param circle The geo circle along which the path lies.
         * @param start The start of the path.
         * @param end The end of the path.
         * @param handler A function to handle the resampled points. The function is called once for each resampled point,
         * in order.
         */
        resample(projection, circle, start, end, handler) {
            let startPoint, startVec, endPoint, endVec;
            if (start instanceof Float64Array) {
                startPoint = this.geoPointCache[0].setFromCartesian(start);
                startVec = start;
            }
            else {
                startPoint = start;
                startVec = GeoPoint.sphericalToCartesian(start, this.vec3Cache[0]);
            }
            if (end instanceof Float64Array) {
                endPoint = this.geoPointCache[0].setFromCartesian(end);
                endVec = end;
            }
            else {
                endPoint = end;
                endVec = GeoPoint.sphericalToCartesian(end, this.vec3Cache[1]);
            }
            const startLat = startPoint.lat;
            const startLon = startPoint.lon;
            const endLat = endPoint.lat;
            const endLon = endPoint.lon;
            const startProjected = projection.project(start, this.vec2Cache[0]);
            const endProjected = projection.project(end, this.vec2Cache[1]);
            const startX = startProjected[0];
            const startY = startProjected[1];
            const endX = endProjected[0];
            const endY = endProjected[1];
            this.startVector.point.set(startLat, startLon);
            Vec2Math.copy(startProjected, this.startVector.projected);
            handler(this.startVector);
            this.state.index = 1;
            this.state.prevX = startX;
            this.state.prevY = startY;
            this.state.vectorType = 'line';
            const state = this.resampleHelper(projection, circle, startLat, startLon, startVec[0], startVec[1], startVec[2], startX, startY, endLat, endLon, endVec[0], endVec[1], endVec[2], endX, endY, handler, 0, this.state);
            this.callHandler(handler, endLat, endLon, endX, endY, state);
        }
        /**
         * Resamples a projected great- or small-circle path. This method will recursively split the path into two halves
         * and resample the midpoint. Based on the projected position of the midpoint relative to those of the start and end
         * points, the projected path is modeled as either a straight line from the start to the end or a circular arc
         * connecting the start, end, and midpoints. Recursion continues as long as the maximum depth has not been reached
         * and at least one of the following conditions is met:
         * * The distance from the midpoint to the endpoints is greater than or equal to the minimum resampling distance.
         * * If the path is modeled as a line: the distance from the projected midpoint to the model line is greater than
         * this resampler's Douglas-Peucker tolerance.
         * * If the path is modeled as an arc: the distance from the projected one-quarter or the three-quarter point along
         * the path to the model arc is greater than this resampler's Douglas-Peucker tolerance.
         * @param projection The projection to use.
         * @param circle The geo circle along which the path lies.
         * @param lat1 The latitude of the start of the path, in degrees.
         * @param lon1 The longitude of the start of the path, in degrees.
         * @param x1 The x-component of the Cartesian position vector of the start of the path.
         * @param y1 The y-component of the Cartesian position vector of the start of the path.
         * @param z1 The z-component of the Cartesian position vector of the start of the path.
         * @param projX1 The x-component of the projected location of the start of the path, in pixels.
         * @param projY1 The y-component of the projected location of the start of the path, in pixels.
         * @param lat2 The latitude of the end of the path, in degrees.
         * @param lon2 The longitude of the end of the path, in degrees.
         * @param x2 The x-component of the Cartesian position vector of the end of the path.
         * @param y2 The y-component of the Cartesian position vector of the end of the path.
         * @param z2 The z-component of the Cartesian position vector of the end of the path.
         * @param projX2 The x-component of the projected location of the end of the path, in pixels.
         * @param projY2 The y-component of the projected location of the end of the path, in pixels.
         * @param handler A function to handle the resampled points.
         * @param depth The current depth of the resampling algorithm.
         * @param state The current state of the resampling algorithm.
         * @returns The index of the next resampled point.
         */
        resampleHelper(projection, circle, lat1, lon1, x1, y1, z1, projX1, projY1, lat2, lon2, x2, y2, z2, projX2, projY2, handler, depth, state) {
            if (depth >= this.maxDepth) {
                return state;
            }
            const startVec = Vec3Math.set(x1, y1, z1, this.vec3Cache[0]);
            const endVec = Vec3Math.set(x2, y2, z2, this.vec3Cache[1]);
            const angularWidth = circle.angleAlong(startVec, endVec, Math.PI);
            if (angularWidth <= GeoCircle.ANGULAR_TOLERANCE) {
                return state;
            }
            const midVec = circle.offsetAngleAlong(startVec, angularWidth / 2, this.vec3Cache[2]);
            const startProjected = Vec2Math.set(projX1, projY1, this.vec2Cache[0]);
            const endProjected = Vec2Math.set(projX2, projY2, this.vec2Cache[1]);
            const deltaProjected = Vec2Math.sub(endProjected, startProjected, this.vec2Cache[2]);
            const deltaProjectedDot = Vec2Math.dot(deltaProjected, deltaProjected);
            const midPoint = this.geoPointCache[0].setFromCartesian(midVec);
            const midProjected = projection.project(midPoint, this.vec2Cache[2]);
            const lat0 = midPoint.lat;
            const lon0 = midPoint.lon;
            const x0 = midVec[0];
            const y0 = midVec[1];
            const z0 = midVec[2];
            const projX0 = midProjected[0];
            const projY0 = midProjected[1];
            const A = projX2 - projX1;
            const B = projY2 - projY1;
            const C = projX1 * projX1 - projX2 * projX2 + projY1 * projY1 - projY2 * projY2;
            const D = projX0 - projX1;
            const E = projY0 - projY1;
            const F = projX1 * projX1 - projX0 * projX0 + projY1 * projY1 - projY0 * projY0;
            // Calculate the Douglas-Peucker metric
            const det = 2 * (A * E - B * D);
            const dpDisSq = (det * det / 4) / deltaProjectedDot;
            if (dpDisSq > this.dpTolSq) {
                // Attempt to model the projected path with an arc
                // Find the center of circle containing the arc passing through the projected start, end, and mid points.
                const arcCenterX = (B * F - C * E) / det;
                const arcCenterY = (C * D - A * F) / det;
                const arcRadius = Math.hypot(arcCenterX - projX1, arcCenterY - projY1);
                const startToEndVec = Vec3Math.set(A, B, 0, this.vec3Cache[3]);
                const centerToMidVec = Vec3Math.set(projX0 - arcCenterX, projY0 - arcCenterY, 0, this.vec3Cache[4]);
                const cross = Vec3Math.cross(startToEndVec, centerToMidVec, this.vec3Cache[4]);
                state.vectorType = 'arc';
                state.arcCenterX = arcCenterX;
                state.arcCenterY = arcCenterY;
                state.arcRadius = arcRadius;
                state.isArcCounterClockwise = cross[2] > 0;
            }
            else {
                state.vectorType = 'line';
            }
            const cosDistance = Vec3Math.dot(startVec, midVec);
            if (cosDistance > this.cosMinDistance) { // cosine of distance increases with decreasing distance
                // We are below the minimum distance required to continue resampling -> decide if we need to continue or if
                // the path can satisfactorily be modeled as either a straight line or a circular arc.
                if (state.vectorType === 'line') {
                    // The path can be modeled as a line.
                    return state;
                }
                // To find whether the path can be modeled as an arc, we need to project the one-quarter and three-quarter points
                // along the path and find the projected points' distances from the arc modeled above. If the distances are
                // within the D-P tolerance, then the path can be modeled as an arc.
                const query = circle.offsetAngleAlong(startVec, angularWidth / 4, this.geoPointCache[0]);
                const projectedQuery = projection.project(query, this.vec2Cache[0]);
                let distance = Math.hypot(projectedQuery[0] - state.arcCenterX, projectedQuery[1] - state.arcCenterY);
                if ((distance - state.arcRadius) * (distance - state.arcRadius) <= this.dpTolSq) {
                    circle.offsetAngleAlong(startVec, 3 * angularWidth / 4, query);
                    projection.project(query, projectedQuery);
                    distance = Math.hypot(projectedQuery[0] - state.arcCenterX, projectedQuery[1] - state.arcCenterY);
                    if ((distance - state.arcRadius) * (distance - state.arcRadius) <= this.dpTolSq) {
                        return state;
                    }
                }
            }
            state = this.resampleHelper(projection, circle, lat1, lon1, x1, y1, z1, projX1, projY1, lat0, lon0, x0, y0, z0, projX0, projY0, handler, depth + 1, state);
            this.callHandler(handler, lat0, lon0, projX0, projY0, state);
            state.index++;
            state.prevX = projX0;
            state.prevY = projY0;
            return this.resampleHelper(projection, circle, lat0, lon0, x0, y0, z0, projX0, projY0, lat2, lon2, x2, y2, z2, projX2, projY2, handler, depth + 1, state);
        }
        /**
         * Calls a handler function for a resampled point.
         * @param handler The handler function to call.
         * @param lat The latitude of the resampled point, in degrees.
         * @param lon The longitude of the resampled point, in degrees.
         * @param projX The x-coordinate of the projected resampled point, in pixels.
         * @param projY The y-coordinate of the projected resampled point, in pixels.
         * @param state The current state of the resampling algorithm.
         */
        callHandler(handler, lat, lon, projX, projY, state) {
            let vector;
            if (state.vectorType === 'line') {
                vector = this.lineVector;
            }
            else {
                vector = this.arcVector;
                Vec2Math.set(state.arcCenterX, state.arcCenterY, vector.projectedArcCenter);
                vector.projectedArcRadius = state.arcRadius;
                vector.projectedArcStartAngle = Math.atan2(state.prevY - state.arcCenterY, state.prevX - state.arcCenterX);
                vector.projectedArcEndAngle = Math.atan2(projY - state.arcCenterY, projX - state.arcCenterX);
                if (vector.projectedArcEndAngle < vector.projectedArcStartAngle !== state.isArcCounterClockwise) {
                    vector.projectedArcEndAngle += state.isArcCounterClockwise ? -MathUtils.TWO_PI : MathUtils.TWO_PI;
                }
            }
            vector.point.set(lat, lon);
            Vec2Math.set(projX, projY, vector.projected);
            vector.index = state.index;
            handler(vector);
        }
    }

    /**
     * The possible reference norths for navigation angle units.
     */
    exports.NavAngleUnitReferenceNorth = void 0;
    (function (NavAngleUnitReferenceNorth) {
        NavAngleUnitReferenceNorth["True"] = "true";
        NavAngleUnitReferenceNorth["Magnetic"] = "magnetic";
    })(exports.NavAngleUnitReferenceNorth || (exports.NavAngleUnitReferenceNorth = {}));
    /**
     * A basic implementation of a navigation angle unit.
     */
    class BasicNavAngleUnit extends AbstractUnit {
        /** @inheritdoc */
        get magVar() {
            return this._magVar;
        }
        /**
         * Constructor.
         * @param referenceNorth The reference north of the new unit.
         * @param magVar The initial magnetic variation of the new unit.
         */
        constructor(referenceNorth, magVar) {
            super(referenceNorth === exports.NavAngleUnitReferenceNorth.True ? 'true bearing' : 'magnetic bearing');
            /** @inheritdoc */
            this.family = 'navangle';
            this._magVar = 0;
            this._magVar = magVar;
        }
        /**
         * Checks whether this nav angle unit is relative to magnetic north.
         * @returns Whether this nav angle unit is relative to magnetic north.
         */
        isMagnetic() {
            return this.name === 'magnetic bearing';
        }
        /**
         * Converts a value of this unit to another unit. This unit's magnetic variation is used for the conversion.
         * @param value The value to convert.
         * @param toUnit The unit to which to convert.
         * @returns The converted value.
         * @throws Error if attempting an invalid conversion.
         */
        convertTo(value, toUnit) {
            if (!this.canConvert(toUnit)) {
                throw new Error(`Invalid conversion from ${this.name} to ${toUnit.name}.`);
            }
            if (!isFinite(value)) {
                return NaN;
            }
            if (this.isMagnetic() === toUnit.isMagnetic()) {
                return value;
            }
            return this.isMagnetic() ? MagVar.magneticToTrue(value, this.magVar) : MagVar.trueToMagnetic(value, this.magVar);
        }
        /**
         * Converts a value of another unit to this unit. This unit's magnetic variation is used for the conversion.
         * @param value The value to convert.
         * @param fromUnit The unit from which to convert.
         * @returns The converted value.
         * @throws Error if attempting an invalid conversion.
         */
        convertFrom(value, fromUnit) {
            if (!this.canConvert(fromUnit)) {
                throw new Error(`Invalid conversion from ${fromUnit.name} to ${this.name}.`);
            }
            if (!isFinite(value)) {
                return NaN;
            }
            if (this.isMagnetic() === fromUnit.isMagnetic()) {
                return value;
            }
            return this.isMagnetic() ? MagVar.trueToMagnetic(value, this.magVar) : MagVar.magneticToTrue(value, this.magVar);
        }
        /**
         * Sets this unit's magnetic variation.
         * @param magVar The magnetic variation to set, in degrees.
         */
        setMagVar(magVar) {
            this._magVar = magVar;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setMagVarFromLocation(arg1, arg2) {
            if (typeof arg1 === 'number') {
                this._magVar = MagVar.get(arg1, arg2);
            }
            else {
                this._magVar = MagVar.get(arg1);
            }
        }
        /** @inheritdoc */
        equals(other) {
            return other instanceof BasicNavAngleUnit && this.name === other.name && this.magVar === other.magVar;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(isMagnetic, arg2, arg3) {
            const referenceNorth = isMagnetic ? exports.NavAngleUnitReferenceNorth.Magnetic : exports.NavAngleUnitReferenceNorth.True;
            let magVar = 0;
            if (arg2 !== undefined) {
                if (typeof arg2 === 'number') {
                    if (arg3 === undefined) {
                        magVar = arg2;
                    }
                    else {
                        magVar = MagVar.get(arg2, arg3);
                    }
                }
                else {
                    magVar = MagVar.get(arg2);
                }
            }
            return new BasicNavAngleUnit(referenceNorth, magVar);
        }
    }
    /**
     * A Subject which provides a navigation angle value.
     */
    class BasicNavAngleSubject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param value The value of this subject.
         */
        constructor(value) {
            super();
            this.value = value;
            /** @inheritdoc */
            this.isMutableSubscribable = true;
        }
        /**
         * Creates a BasicNavAngleSubject.
         * @param initialVal The initial value.
         * @returns A BasicNavAngleSubject.
         */
        static create(initialVal) {
            return new BasicNavAngleSubject(initialVal);
        }
        /** @inheritdoc */
        get() {
            return this.value.readonly;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, arg2, arg3) {
            const isArg1Number = typeof arg1 === 'number';
            const isArg2Number = typeof arg2 === 'number';
            const isArg2LatLon = typeof arg2 === 'object' && 'lat' in arg2 && 'lon' in arg2;
            const unit = isArg1Number
                ? isArg2Number || isArg2LatLon || arg2 === undefined ? this.value.unit : arg2
                : arg1.unit;
            const oldMagVar = this.value.unit.magVar;
            const oldValue = this.value.number;
            if (isArg2LatLon) {
                this.value.unit.setMagVarFromLocation(arg2);
            }
            else if (isArg2Number) {
                if (typeof arg3 === 'number') {
                    this.value.unit.setMagVarFromLocation(arg2, arg3);
                }
                else {
                    this.value.unit.setMagVar(arg2);
                }
            }
            else {
                this.value.unit.setMagVar(unit.magVar);
            }
            if (isArg1Number) {
                this.value.set(arg1, unit);
            }
            else {
                this.value.set(arg1);
            }
            if ((!(isNaN(oldMagVar) && isNaN(this.value.unit.magVar)) && oldMagVar !== this.value.unit.magVar)
                || (!(isNaN(oldValue) && isNaN(this.value.number)) && oldValue !== this.value.number)) {
                this.notify();
            }
        }
    }
    BasicNavAngleSubject.TRUE_BEARING = BasicNavAngleUnit.create(false);

    /**
     * The transition type to which a flight path vector belongs.
     */
    exports.FlightPathVectorFlags = void 0;
    (function (FlightPathVectorFlags) {
        FlightPathVectorFlags[FlightPathVectorFlags["None"] = 0] = "None";
        /** A turn to a specific course. */
        FlightPathVectorFlags[FlightPathVectorFlags["TurnToCourse"] = 1] = "TurnToCourse";
        /** An arcing turn to a specific point. */
        FlightPathVectorFlags[FlightPathVectorFlags["Arc"] = 2] = "Arc";
        /** A direct course to a specific point. */
        FlightPathVectorFlags[FlightPathVectorFlags["Direct"] = 4] = "Direct";
        /** A path to intercept a specific course. */
        FlightPathVectorFlags[FlightPathVectorFlags["InterceptCourse"] = 8] = "InterceptCourse";
        /** Inbound leg of a hold. */
        FlightPathVectorFlags[FlightPathVectorFlags["HoldInboundLeg"] = 16] = "HoldInboundLeg";
        /** Outbound leg of a hold. */
        FlightPathVectorFlags[FlightPathVectorFlags["HoldOutboundLeg"] = 32] = "HoldOutboundLeg";
        /** A direct hold entry. */
        FlightPathVectorFlags[FlightPathVectorFlags["HoldDirectEntry"] = 64] = "HoldDirectEntry";
        /** A teardrop hold entry. */
        FlightPathVectorFlags[FlightPathVectorFlags["HoldTeardropEntry"] = 128] = "HoldTeardropEntry";
        /** A parallel hold entry. */
        FlightPathVectorFlags[FlightPathVectorFlags["HoldParallelEntry"] = 256] = "HoldParallelEntry";
        /** A course reversal. */
        FlightPathVectorFlags[FlightPathVectorFlags["CourseReversal"] = 512] = "CourseReversal";
        /** A turn from one leg to another. */
        FlightPathVectorFlags[FlightPathVectorFlags["LegToLegTurn"] = 1024] = "LegToLegTurn";
        /** An anticipated turn from one leg to another. */
        FlightPathVectorFlags[FlightPathVectorFlags["AnticipatedTurn"] = 2048] = "AnticipatedTurn";
        /** A fallback path. */
        FlightPathVectorFlags[FlightPathVectorFlags["Fallback"] = 4096] = "Fallback";
        /** A path through a discontinuity. */
        FlightPathVectorFlags[FlightPathVectorFlags["Discontinuity"] = 8192] = "Discontinuity";
        /** The constant-heading vector in a fly-heading leg. */
        FlightPathVectorFlags[FlightPathVectorFlags["ConstantHeading"] = 16384] = "ConstantHeading";
    })(exports.FlightPathVectorFlags || (exports.FlightPathVectorFlags = {}));

    /**
     * Utility class for working with flight path calculations.
     */
    class FlightPathUtils {
        /**
         * Creates an empty flight path vector.
         * @returns An empty flight path vector.
         */
        static createEmptyVector() {
            return {
                flags: exports.FlightPathVectorFlags.None,
                radius: 0,
                centerX: 1,
                centerY: 0,
                centerZ: 0,
                startLat: 0,
                startLon: 0,
                endLat: 0,
                endLon: 0,
                distance: 0,
                heading: null,
                isHeadingTrue: false
            };
        }
        /**
         * Sets the parameters of a flight path vector using a GeoCircle.
         * @param vector The fligh path vector to set.
         * @param circle The GeoCircle defining the vector's path.
         * @param start The start of the vector.
         * @param end The end of the vector.
         * @param flags The flags to set on the vector.
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The circle vector, after its parameters have been set.
         */
        static setVectorFromCircle(vector, circle, start, end, flags, heading = null, isHeadingTrue = false) {
            vector.flags = flags;
            vector.radius = circle.radius;
            vector.centerX = circle.center[0];
            vector.centerY = circle.center[1];
            vector.centerZ = circle.center[2];
            vector.distance = UnitType.GA_RADIAN.convertTo(circle.distanceAlong(start, end, Math.PI), UnitType.METER);
            start instanceof Float64Array && (start = FlightPathUtils.geoPointCache[0].setFromCartesian(start));
            end instanceof Float64Array && (end = FlightPathUtils.geoPointCache[1].setFromCartesian(end));
            vector.startLat = start.lat;
            vector.startLon = start.lon;
            vector.endLat = end.lat;
            vector.endLon = end.lon;
            vector.heading = heading;
            vector.isHeadingTrue = isHeadingTrue;
            return vector;
        }
        /**
         * Checks whether a circle vector describes a great-circle path.
         * @param vector A flight path circle vector.
         * @returns Whether the vector describes a great-circle path.
         */
        static isVectorGreatCircle(vector) {
            return vector.radius === Math.PI / 2;
        }
        /**
         * Sets the parameters of a GeoCircle from a flight path circle vector.
         * @param vector A flight path circle vector.
         * @param out The GeoCircle to set.
         * @returns The GeoCircle, after its parameters have been set.
         */
        static setGeoCircleFromVector(vector, out) {
            return out.set(Vec3Math.set(vector.centerX, vector.centerY, vector.centerZ, FlightPathUtils.vec3Cache[0]), vector.radius);
        }
        /**
         * Gets the direction of a turn described by a flight path circle vector.
         * @param vector The flight path circle vector describing the turn.
         * @returns The direction of the turn described by the flight path circle vector.
         */
        static getVectorTurnDirection(vector) {
            return vector.radius > MathUtils.HALF_PI ? 'right' : 'left';
        }
        /**
         * Gets the radius of a turn described by a flight path circle vector.
         * @param vector The flight path circle vector describing the turn.
         * @returns The radius of the turn described by the flight path circle vector, in great-arc radians.
         */
        static getVectorTurnRadius(vector) {
            return Math.min(vector.radius, Math.PI - vector.radius);
        }
        /**
         * Gets the initial true course bearing of a flight path vector.
         * @param vector A flight path vector.
         * @returns The initial true course bearing of the vector, or undefined if one could not be calculated.
         */
        static getVectorInitialCourse(vector) {
            return FlightPathUtils.setGeoCircleFromVector(vector, FlightPathUtils.geoCircleCache[0]).bearingAt(FlightPathUtils.geoPointCache[0].set(vector.startLat, vector.startLon), Math.PI);
        }
        /**
         * Gets the final true course bearing of a flight path vector.
         * @param vector A flight path vector.
         * @returns The final true course bearing of the vector, or `undefined` if one could not be calculated.
         */
        static getVectorFinalCourse(vector) {
            return FlightPathUtils.setGeoCircleFromVector(vector, FlightPathUtils.geoCircleCache[0]).bearingAt(FlightPathUtils.geoPointCache[0].set(vector.endLat, vector.endLon), Math.PI);
        }
        /**
         * Gets the desired turn direction defined by a flight plan leg.
         * @param leg A flight plan leg.
         * @returns The desired turn direction defined by the specified flight plan leg, or `undefined` if the leg does not
         * define a desired turn direction.
         */
        static getLegDesiredTurnDirection(leg) {
            return leg.turnDirection === exports.LegTurnDirection.Left ? 'left' : leg.turnDirection === exports.LegTurnDirection.Right ? 'right' : undefined;
        }
        /**
         * Gets the final position of a calculated leg.
         * @param legCalc A set of leg calculations.
         * @param out The GeoPoint object to which to write the result.
         * @returns The final position of the leg, or `undefined` if one could not be obtained.
         */
        static getLegFinalPosition(legCalc, out) {
            if (legCalc.endLat !== undefined && legCalc.endLon !== undefined) {
                return out.set(legCalc.endLat, legCalc.endLon);
            }
            return undefined;
        }
        /**
         * Gets the final true course of a calculated leg.
         * @param legCalc A set of leg calculations.
         * @returns The final true course of the leg, or `undefined` if one could not be obtained.
         */
        static getLegFinalCourse(legCalc) {
            if (legCalc.flightPath.length > 0) {
                const vector = legCalc.flightPath[legCalc.flightPath.length - 1];
                return this.getVectorFinalCourse(vector);
            }
            return undefined;
        }
        /**
         * Gets the circle describing the path of a turn.
         * @param center The center of the turn.
         * @param radius The radius of the turn, in great-arc radians.
         * @param turnDirection The direction of the turn.
         * @param out A GeoCircle object to which to write the result.
         * @returns The circle describing the path of the turn.
         */
        static getTurnCircle(center, radius, turnDirection, out) {
            out.set(center, radius);
            if (turnDirection === 'right') {
                out.reverse();
            }
            return out;
        }
        /**
         * Reverses the direction of a turn circle while keeping the turn center and turn radius constant.
         * @param circle The turn circle to reverse.
         * @param out A GeoCircle object to which to write the result.
         * @returns A turn circle which has the same turn center and turn radius, but the opposite direction as `circle`.
         */
        static reverseTurnCircle(circle, out) {
            return out.set(Vec3Math.multScalar(circle.center, -1, FlightPathUtils.vec3Cache[0]), Math.PI - circle.radius);
        }
        /**
         * Gets the direction of a turn described by a circle.
         * @param circle The geo circle describing the turn.
         * @returns The direction of the turn described by the circle.
         */
        static getTurnDirectionFromCircle(circle) {
            return circle.radius > MathUtils.HALF_PI ? 'right' : 'left';
        }
        /**
         * Gets the radius of a turn described by a circle.
         * @param circle The geo circle describing the turn.
         * @returns The radius of the turn described by the circle, in great-arc radians.
         */
        static getTurnRadiusFromCircle(circle) {
            return Math.min(circle.radius, Math.PI - circle.radius);
        }
        /**
         * Gets the center of a turn described by a circle.
         * @param circle The geo circle describing the turn.
         * @param out A GeoPoint or 3D vector object to which to write the result.
         * @returns The center of a turn described by the circle.
         */
        static getTurnCenterFromCircle(circle, out) {
            return (circle.radius > MathUtils.HALF_PI
                ? out instanceof Float64Array
                    ? Vec3Math.multScalar(circle.center, -1, out)
                    : out.setFromCartesian(-circle.center[0], -circle.center[1], -circle.center[2])
                : out instanceof Float64Array
                    ? Vec3Math.copy(circle.center, out)
                    : out.setFromCartesian(circle.center));
        }
        /**
         * Gets the direction of a turn from an initial course to a final course that sweeps through the least angular
         * distance.
         * @param initialCourse The initial course, in degrees.
         * @param finalCourse The final course, in degrees.
         * @returns The direction of the turn from the specified initial course to final course that sweeps through the
         * least angular distance, or `undefined` if the two courses are exactly equal or exactly 180 degrees apart.
         */
        static getShortestTurnDirection(initialCourse, finalCourse) {
            const diff = MathUtils.angularDistanceDeg(initialCourse, finalCourse, 1);
            if (diff === 0 || diff === 180) {
                return undefined;
            }
            else {
                return diff > 180 ? 'left' : 'right';
            }
        }
        /**
         * Gets the angular distance, in radians, swept when turning from an initial path to a final path at a given point.
         * @param initialPath The initial path.
         * @param finalPath The final path.
         * @param point The point at which to turn from the initial path to the final path. Must lie on both paths.
         * @param turnDirection The direction in which to turn from the initial path to the final path, or `null` to choose
         * the turn direction that results in the smallest angular distance.
         * @param tolerance The error tolerance, in great-arc radians, to use when checking whether `point` lies on both
         * `initialPath` and `finalPath`. Defaults to {@link GeoMath.ANGULAR_TOLERANCE}.
         * @returns The angular distance, in radians, swept when turning from the specified initial path to the final path at
         * the specified point, or `NaN` if an angular distance could not be calculated.
         * @throws Error if `point` does not lie on both `initialPath` and `finalPath`.
         */
        static pathAngleDistance(initialPath, finalPath, point, turnDirection, tolerance = GeoMath.ANGULAR_TOLERANCE) {
            if (!(point instanceof Float64Array)) {
                point = GeoPoint.sphericalToCartesian(point, FlightPathUtils.vec3Cache[0]);
            }
            if (!initialPath.includes(point, tolerance) || !finalPath.includes(point, tolerance)) {
                throw Error('FlightPathUtils::pathAngleDistance(): the specified point does not lie on both the initial and final paths');
            }
            const initialPathNormal = initialPath.isGreatCircle()
                ? initialPath.center
                : GeoCircle.getGreatCircleNormal(point, initialPath, FlightPathUtils.vec3Cache[1]);
            if (!Vec3Math.isFinite(initialPathNormal)) {
                return NaN;
            }
            const finalPathNormal = finalPath.isGreatCircle()
                ? finalPath.center
                : GeoCircle.getGreatCircleNormal(point, finalPath, FlightPathUtils.vec3Cache[2]);
            if (!Vec3Math.isFinite(finalPathNormal)) {
                return NaN;
            }
            const angleDiff = Vec3Math.unitAngle(initialPathNormal, finalPathNormal);
            const initialPathRadialNormal = Vec3Math.normalize(Vec3Math.cross(initialPathNormal, point, FlightPathUtils.vec3Cache[3]), FlightPathUtils.vec3Cache[1]);
            if (!Vec3Math.isFinite(initialPathRadialNormal)) {
                return NaN;
            }
            if (turnDirection === null) {
                return angleDiff;
            }
            const angleSign = Math.sign(Vec3Math.dot(initialPathRadialNormal, finalPathNormal));
            if (turnDirection === 'right') {
                return MathUtils.normalizeAngle(angleDiff * angleSign);
            }
            else {
                return MathUtils.normalizeAngle(angleDiff * angleSign * -1);
            }
        }
        /**
         * Gets the great circle tangent to a given path at a given tangent point. The tangent circle will contain the
         * tangent point and have the same direction as the path at the tangent point.
         * @param point The tangent point. If the point does not lie on the path, it will be projected onto the path.
         * @param path The geo circle describing the path.
         * @param out A GeoCircle object to which to write the result.
         * @returns The great circle tangent to the specified path at the specified point.
         */
        static getGreatCircleTangentToPath(point, path, out) {
            return out.setAsGreatCircle(point, path);
        }
        /**
         * Gets the great circle tangent to a given flight path vector at a given tangent point. The tangent circle will
         * contain the tangent point and have the same direction as the vector at the tangent point.
         * @param point The tangent point. If the point does not lie on the vector, it will be projected onto the vector.
         * @param vector The flight path vector.
         * @param out A GeoCircle object to which to write the result.
         * @returns The great circle tangent to the specified flight path vector at the specified point.
         */
        static getGreatCircleTangentToVector(point, vector, out) {
            const vectorCircle = FlightPathUtils.setGeoCircleFromVector(vector, FlightPathUtils.geoCircleCache[0]);
            return out.setAsGreatCircle(point, vectorCircle);
        }
        /**
         * Calculates and returns a circle describing a turn starting from a path at a specified point.
         * @param start The starting point of the turn.
         * @param path The circle describing the path from which the turn starts.
         * @param turnRadius The radius of the turn, in great-arc radians.
         * @param turnDirection The direction of the turn.
         * @param out A GeoCircle object to which to write the result.
         * @returns The circle describing the path of the specified turn.
         */
        static getTurnCircleStartingFromPath(start, path, turnRadius, turnDirection, out) {
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, FlightPathUtils.vec3Cache[0]);
            }
            const radius = turnDirection === 'left'
                ? turnRadius
                : Math.PI - turnRadius;
            const turnStartToCenterNormal = Vec3Math.cross(start, path.center, FlightPathUtils.vec3Cache[1]);
            const turnStartToCenterPath = FlightPathUtils.geoCircleCache[0].set(turnStartToCenterNormal, MathUtils.HALF_PI);
            const turnCenter = turnStartToCenterPath.offsetDistanceAlong(start, radius, FlightPathUtils.vec3Cache[1], Math.PI);
            return out.set(turnCenter, radius);
        }
        /**
         * Gets the signed distance along an arc from a defined start point to a query point. The start, query, and end
         * points will be projected onto the arc's parent circle if they do not already lie on it. A negative distance
         * indicates that the query point lies somewhere before the start of the arc but after the point on the arc's parent
         * circle that is diametrically opposed to the midpoint of the arc.
         * @param circle The arc's parent circle.
         * @param start The start point of the arc.
         * @param end The end point of the arc.
         * @param pos The query point.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `start` and `query` are equal.
         * Defaults to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns The signed distance along the arc from the start point to the query point, in great-arc radians.
         */
        static getAlongArcSignedDistance(circle, start, end, pos, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const posAngularDistance = circle.angleAlong(start, pos, Math.PI);
            if (Math.min(posAngularDistance, MathUtils.TWO_PI - posAngularDistance) <= tolerance) {
                return 0;
            }
            const endAngularDistance = circle.angleAlong(start, end, Math.PI);
            return circle.arcLength((posAngularDistance - (endAngularDistance / 2) + Math.PI) % MathUtils.TWO_PI - Math.PI + endAngularDistance / 2);
        }
        /**
         * Gets the normalized distance along an arc from a defined start point to a query point. The start, query, and end
         * points will be projected onto the arc's parent circle if they do not already lie on it. The distance is normalized
         * such that 1 equals the arc length from the start point to the end point. A negative distance indicates that the
         * query point lies somewhere before the start of the arc but after the point on the arc's parent circle that is
         * diametrically opposed to the midpoint of the arc.
         * @param circle The arc's parent circle.
         * @param start The start point of the arc.
         * @param end The end point of the arc.
         * @param pos The query point.
         * @param tolerance The error tolerance, in great-arc radians, when checking if `start` and `query` are equal.
         * Defaults to `GeoCircle.ANGULAR_TOLERANCE` if not specified.
         * @returns The normalized distance along the arc from the start point to the query point.
         */
        static getAlongArcNormalizedDistance(circle, start, end, pos, tolerance = GeoCircle.ANGULAR_TOLERANCE) {
            const posAngularDistance = circle.angleAlong(start, pos, Math.PI);
            if (Math.min(posAngularDistance, MathUtils.TWO_PI - posAngularDistance) <= tolerance) {
                return 0;
            }
            const endAngularDistance = circle.angleAlong(start, end, Math.PI);
            if (Math.min(endAngularDistance, MathUtils.TWO_PI - endAngularDistance) <= tolerance) {
                return posAngularDistance >= Math.PI ? -Infinity : Infinity;
            }
            return ((posAngularDistance - (endAngularDistance / 2) + Math.PI) % MathUtils.TWO_PI - Math.PI) / endAngularDistance + 0.5;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static isPointAlongArc(circle, start, end, pos, inclusive = true, tolerance = GeoMath.ANGULAR_TOLERANCE) {
            const angularTolerance = circle.angularWidth(tolerance);
            if (typeof end !== 'number') {
                end = circle.angleAlong(start, end, Math.PI, angularTolerance);
            }
            if (inclusive && Math.abs(end) >= MathUtils.TWO_PI - angularTolerance) {
                return true;
            }
            const angle = circle.angleAlong(start, pos, Math.PI);
            if (inclusive && angle >= MathUtils.TWO_PI - angularTolerance) {
                return true;
            }
            const signedDiff = (angle - end) * (end >= 0 ? 1 : -1);
            return inclusive ? signedDiff <= angularTolerance : signedDiff < -angularTolerance;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static projectVelocityToCircle(speed, position, direction, projectTo) {
            if (projectTo.radius <= GeoCircle.ANGULAR_TOLERANCE) {
                return NaN;
            }
            if (speed === 0) {
                return 0;
            }
            if (!(position instanceof Float64Array)) {
                position = GeoPoint.sphericalToCartesian(position, FlightPathUtils.vec3Cache[0]);
            }
            const velocityPath = typeof direction === 'number'
                ? FlightPathUtils.geoCircleCache[0].setAsGreatCircle(position, direction)
                : direction.isGreatCircle()
                    ? direction
                    : FlightPathUtils.geoCircleCache[0].setAsGreatCircle(position, FlightPathUtils.geoCircleCache[0].setAsGreatCircle(direction.center, position).center);
            const sign = velocityPath.encircles(projectTo.center) ? 1 : -1;
            const velocityPathNormal = Vec3Math.copy(velocityPath.center, FlightPathUtils.vec3Cache[1]);
            const projectedRadialNormal = FlightPathUtils.geoCircleCache[0].setAsGreatCircle(projectTo.center, position).center;
            const dot = Vec3Math.dot(projectedRadialNormal, velocityPathNormal);
            const sinTheta = Math.sqrt(1 - MathUtils.clamp(dot * dot, 0, 1));
            return speed * sinTheta * sign;
        }
        /**
         * Resolves the ingress to egress vectors for a set of flight plan leg calculations. This operation will populate the
         * `ingressToEgress` array with a sequence of vectors connecting the ingress transition to the egress transition
         * while following the flight path defined by the vectors in the `flightPath` array.
         * @param legCalc A set of flight plan leg calculations.
         * @returns The flight plan leg calculations, after the ingress to egress vectors have been resolved.
         */
        static resolveIngressToEgress(legCalc) {
            var _a, _b, _c, _d, _e, _f;
            var _g, _h, _j, _k, _l, _m;
            const vectors = legCalc.ingressToEgress;
            let vectorIndex = 0;
            let flightPathVectorIndex = Math.max(0, legCalc.ingressJoinIndex);
            const lastIngressVector = legCalc.ingress[legCalc.ingress.length - 1];
            const ingressJoinVector = legCalc.flightPath[legCalc.ingressJoinIndex];
            const firstEgressVector = legCalc.egress[0];
            const egressJoinVector = legCalc.flightPath[legCalc.egressJoinIndex];
            if (lastIngressVector && ingressJoinVector) {
                // There is an ingress transition and a valid base flight path vector at which the ingress joins the base flight path.
                // Check if the last ingress vector joins the base flight path before the end of the joined base flight path vector.
                const ingressEnd = FlightPathUtils.geoPointCache[0].set(lastIngressVector.endLat, lastIngressVector.endLon);
                const ingressJoinVectorStart = FlightPathUtils.geoPointCache[1].set(ingressJoinVector.startLat, ingressJoinVector.startLon);
                const ingressJoinVectorEnd = legCalc.ingressJoinIndex === legCalc.egressJoinIndex && firstEgressVector
                    ? FlightPathUtils.geoPointCache[2].set(firstEgressVector.startLat, firstEgressVector.startLon)
                    : FlightPathUtils.geoPointCache[2].set(ingressJoinVector.endLat, ingressJoinVector.endLon);
                const ingressJoinVectorCircle = FlightPathUtils.setGeoCircleFromVector(ingressJoinVector, FlightPathUtils.geoCircleCache[0]);
                const ingressEndAlongVectorDistance = FlightPathUtils.getAlongArcNormalizedDistance(ingressJoinVectorCircle, ingressJoinVectorStart, ingressJoinVectorEnd, ingressEnd);
                const normalizedTolerance = GeoMath.ANGULAR_TOLERANCE / UnitType.METER.convertTo(ingressJoinVector.distance, UnitType.GA_RADIAN);
                if (ingressEndAlongVectorDistance < 1 - normalizedTolerance) {
                    // Ingress joins the base flight path before the end of the joined vector. Therefore we must insert all or
                    // part of the joined vector into the ingress-to-egress array.
                    if (ingressEndAlongVectorDistance > normalizedTolerance) {
                        // Ingress joins the base flight path after the start of the joined vector. Therefore we must create a new
                        // vector that extends from where the ingress joins up to the end of the joined vector and insert the new
                        // vector into the ingress-to-egress array.
                        ingressJoinVectorCircle.closest(ingressEnd, ingressEnd);
                        FlightPathUtils.setVectorFromCircle((_a = vectors[_g = vectorIndex++]) !== null && _a !== void 0 ? _a : (vectors[_g] = FlightPathUtils.createEmptyVector()), ingressJoinVectorCircle, ingressEnd, ingressJoinVectorEnd, ingressJoinVector.flags, ingressJoinVector.heading, ingressJoinVector.isHeadingTrue);
                    }
                    else {
                        // Ingress joins the base flight path at or before the start of the joined vector. Therefore we can copy the
                        // entire joined vector into the ingress-to-egress array.
                        Object.assign((_b = vectors[_h = vectorIndex++]) !== null && _b !== void 0 ? _b : (vectors[_h] = FlightPathUtils.createEmptyVector()), ingressJoinVector);
                    }
                }
                flightPathVectorIndex++;
            }
            // Now we must copy all base flight path vectors between the ingress-joined and egress-joined vectors (exclusive)
            // into the ingress-to-egress array.
            const end = Math.min(legCalc.flightPath.length, legCalc.egressJoinIndex < 0 ? Infinity : legCalc.egressJoinIndex);
            for (let i = flightPathVectorIndex; i < end; i++) {
                Object.assign((_c = vectors[_j = vectorIndex++]) !== null && _c !== void 0 ? _c : (vectors[_j] = FlightPathUtils.createEmptyVector()), legCalc.flightPath[i]);
                flightPathVectorIndex++;
            }
            if (flightPathVectorIndex === legCalc.egressJoinIndex && egressJoinVector) {
                if (firstEgressVector) {
                    // There is an egress transition and a valid base flight path vector at which the egress joins the base flight path.
                    // Check if the first egress vector joins the base flight path after the start of the joined base flight path vector.
                    const egressStart = FlightPathUtils.geoPointCache[0].set(firstEgressVector.startLat, firstEgressVector.startLon);
                    const egressJoinVectorStart = FlightPathUtils.geoPointCache[1].set(egressJoinVector.startLat, egressJoinVector.startLon);
                    const egressJoinVectorEnd = FlightPathUtils.geoPointCache[2].set(egressJoinVector.endLat, egressJoinVector.endLon);
                    const egressJoinVectorCircle = FlightPathUtils.setGeoCircleFromVector(egressJoinVector, FlightPathUtils.geoCircleCache[0]);
                    const egressStartAlongVectorDistance = FlightPathUtils.getAlongArcNormalizedDistance(egressJoinVectorCircle, egressJoinVectorStart, egressJoinVectorEnd, egressStart);
                    const normalizedTolerance = GeoMath.ANGULAR_TOLERANCE / UnitType.METER.convertTo(egressJoinVector.distance, UnitType.GA_RADIAN);
                    if (egressStartAlongVectorDistance > normalizedTolerance) {
                        // Egress joins the base flight path after the start of the joined vector. Therefore we must insert all or
                        // part of the joined vector into the ingress-to-egress array.
                        if (egressStartAlongVectorDistance < 1 - normalizedTolerance) {
                            // Egress joins the base flight path before the end of the joined vector. Therefore we must create a new
                            // vector that extends from the start of the joined vector up to where the egress joins and insert the new
                            // vector into the ingress-to-egress array.
                            egressJoinVectorCircle.closest(egressStart, egressStart);
                            FlightPathUtils.setVectorFromCircle((_d = vectors[_k = vectorIndex++]) !== null && _d !== void 0 ? _d : (vectors[_k] = FlightPathUtils.createEmptyVector()), egressJoinVectorCircle, egressJoinVectorStart, egressStart, egressJoinVector.flags, egressJoinVector.heading, egressJoinVector.isHeadingTrue);
                        }
                        else {
                            // Egress joins the base flight path at or after the end of the joined vector. Therefore we can copy the
                            // entire joined vector into the ingress-to-egress array.
                            Object.assign((_e = vectors[_l = vectorIndex++]) !== null && _e !== void 0 ? _e : (vectors[_l] = FlightPathUtils.createEmptyVector()), egressJoinVector);
                        }
                    }
                }
                else {
                    // There is no egress, but there is a base flight path vector flagged as the vector with which the egress
                    // joins. This is technically an invalid state, but we can easily just treat this as a regular "no-egress"
                    // case and copy the entire egress join vector into the resolved vectors array.
                    Object.assign((_f = vectors[_m = vectorIndex++]) !== null && _f !== void 0 ? _f : (vectors[_m] = FlightPathUtils.createEmptyVector()), egressJoinVector);
                }
            }
            vectors.length = vectorIndex;
            return legCalc;
        }
    }
    FlightPathUtils.vec3Cache = ArrayUtils.create(4, () => Vec3Math.create());
    FlightPathUtils.geoPointCache = ArrayUtils.create(3, () => new GeoPoint(0, 0));
    FlightPathUtils.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * An abstract implementation of {@link FlightPathLegCalculator}.
     */
    class AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of AbstractFlightPathLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         * @param canSkipWhenActive Whether this calculator can skip leg calculations when the leg to calculate is the active
         * flight plan leg.
         */
        constructor(facilityCache, canSkipWhenActive) {
            this.facilityCache = facilityCache;
            this.canSkipWhenActive = canSkipWhenActive;
        }
        /**
         * Gets a geographical position from an ICAO string.
         * @param icao An ICAO string.
         * @param out A GeoPoint object to which to write the result.
         * @returns The geographical position corresponding to the ICAO string, or undefined if one could not be obtained.
         */
        getPositionFromIcao(icao, out) {
            const facility = this.facilityCache.get(icao);
            return facility ? out.set(facility) : undefined;
        }
        /**
         * Gets the magnetic variation, in degrees, at a facility. If the facility is a VOR and it has a nominal database
         * magnetic variation, then that value will be returned. Otherwise, the model magnetic variation at the facility's
         * position will be returned.
         * @param icao The ICAO of the facility.
         * @returns The magnetic variation, in degrees, at the specified facility, or `undefined` if the specified facility
         * could not be retrieved.
         */
        getMagVarFromIcao(icao) {
            const facility = this.facilityCache.get(icao);
            if (facility) {
                if (FacilityUtils.isFacilityType(facility, exports.FacilityType.VOR)) {
                    return -facility.magneticVariation;
                }
                // Fall back to getting the model magvar at the facility if we couldn't get a magvar from a VOR.
                return MagVar.get(facility);
            }
            return undefined;
        }
        /**
         * Gets the geographic position for a flight plan leg terminator.
         * @param leg A flight plan leg.
         * @param out A GeoPoint object to which to write the result.
         * @param icao The ICAO string of the leg's terminator fix. If not defined, then the terminator fix will be retrieved
         * from the flight plan leg, if necessary.
         * @returns The position of the leg terminator, or `undefined` if it could not be determined.
         */
        getTerminatorPosition(leg, out, icao) {
            var _a;
            if (leg.lat !== undefined && leg.lon !== undefined) {
                return out.set(leg.lat, leg.lon);
            }
            else {
                if (icao == undefined) {
                    switch (leg.type) {
                        case exports.LegType.AF:
                        case exports.LegType.CF:
                        case exports.LegType.DF:
                        case exports.LegType.HA:
                        case exports.LegType.HF:
                        case exports.LegType.HM:
                        case exports.LegType.IF:
                        case exports.LegType.RF:
                        case exports.LegType.TF:
                            return this.getPositionFromIcao(leg.fixIcao, out);
                        case exports.LegType.FC: {
                            const origin = this.getPositionFromIcao(leg.fixIcao, out);
                            if (origin) {
                                const course = leg.trueDegrees
                                    ? leg.course
                                    : MagVar.magneticToTrue(leg.course, (_a = this.getMagVarFromIcao(leg.fixIcao)) !== null && _a !== void 0 ? _a : MagVar.get(origin));
                                return origin.offset(course, UnitType.METER.convertTo(leg.distance, UnitType.GA_RADIAN));
                            }
                        }
                    }
                    return undefined;
                }
                else {
                    return this.getPositionFromIcao(icao, out);
                }
            }
        }
        /**
         * Gets the magnetic variation, in degrees, at a flight plan leg's terminator fix. If the fix is a VOR and it has a
         * nominal database magnetic variation, then that value will be returned. Otherwise, the model magnetic variation at
         * the fix's position will be returned.
         * @param leg A flight plan leg.
         * @returns The magnetic variation, in degrees, at the specified flight plan leg's terminator fix, or `undefined` if
         * the magnetic variation could not be determined.
         */
        getTerminatorMagVar(leg) {
            if (leg.lat !== undefined && leg.lon !== undefined) {
                return MagVar.get(leg.lat, leg.lon);
            }
            else {
                return this.getMagVarFromIcao(leg.fixIcao);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        getLegMagVar(leg, defaultPoint) {
            const facIcao = (leg.originIcao && ICAO.isFacility(leg.originIcao, exports.FacilityType.VOR)) ? leg.originIcao
                : (leg.fixIcao && ICAO.isFacility(leg.fixIcao, exports.FacilityType.VOR)) ? leg.fixIcao
                    : undefined;
            const facility = facIcao !== undefined ? this.facilityCache.get(facIcao) : undefined;
            return facility
                // The sign of magnetic variation on VOR facilities is the opposite of the standard east = positive convention.
                ? -facility.magneticVariation
                : defaultPoint ? MagVar.get(defaultPoint) : undefined;
        }
        /**
         * Gets the true course, in degrees, for a flight plan leg. If the leg defines an origin or fix VOR facility, then
         * the magnetic variation defined at the VOR is used to adjust magnetic course. Otherwise the computed magnetic
         * variation for the specified point is used.
         * @param leg A flight plan leg.
         * @returns The true course, in degrees, for the flight plan leg.
         */
        getLegTrueCourse(leg) {
            var _a;
            switch (leg.type) {
                case exports.LegType.CF:
                case exports.LegType.HF:
                case exports.LegType.HM:
                case exports.LegType.HA: {
                    const terminatorPos = this.getTerminatorPosition(leg, AbstractFlightPathLegCalculator.__geoPointCache[0], leg.fixIcao);
                    if (terminatorPos) {
                        return leg.trueDegrees ? leg.course : MagVar.magneticToTrue(leg.course, this.getLegMagVar(leg, terminatorPos));
                    }
                    break;
                }
                case exports.LegType.FA:
                case exports.LegType.FC:
                case exports.LegType.FD:
                case exports.LegType.FM: {
                    const originPos = this.getPositionFromIcao(leg.fixIcao, AbstractFlightPathLegCalculator.__geoPointCache[0]);
                    if (originPos) {
                        return leg.trueDegrees ? leg.course : MagVar.magneticToTrue(leg.course, this.getLegMagVar(leg, originPos));
                    }
                    break;
                }
                case exports.LegType.CA:
                case exports.LegType.CR:
                case exports.LegType.CI:
                case exports.LegType.VA:
                case exports.LegType.VD:
                case exports.LegType.VI:
                case exports.LegType.VM:
                case exports.LegType.VR:
                    return leg.trueDegrees ? leg.course : MagVar.magneticToTrue(leg.course, (_a = this.getMagVarFromIcao(leg.originIcao)) !== null && _a !== void 0 ? _a : 0);
                case exports.LegType.IF:
                    // If the leg is an IF for a runway fix, then use runway heading as the course.
                    if (ICAO.isFacility(leg.fixIcao, exports.FacilityType.RWY)) {
                        const facility = this.facilityCache.get(leg.fixIcao);
                        if (facility && FacilityUtils.isFacilityType(facility, exports.FacilityType.RWY)) {
                            return facility.runway.course;
                        }
                    }
            }
            return undefined;
        }
        /** @inheritDoc */
        calculate(legs, calculateIndex, activeLegIndex, state, options) {
            var _a, _b;
            var _c;
            const calcs = (_a = (_c = legs[calculateIndex]).calculated) !== null && _a !== void 0 ? _a : (_c.calculated = {
                courseMagVar: 0,
                startLat: undefined,
                startLon: undefined,
                endLat: undefined,
                endLon: undefined,
                distance: 0,
                distanceWithTransitions: 0,
                initialDtk: undefined,
                cumulativeDistance: 0,
                cumulativeDistanceWithTransitions: 0,
                flightPath: [],
                ingress: [],
                ingressJoinIndex: -1,
                ingressToEgress: [],
                egressJoinIndex: -1,
                egress: [],
                endsInDiscontinuity: false,
                endsInFallback: false
            });
            const vectors = calcs.flightPath;
            if (this.canSkipWhenActive && activeLegIndex === calculateIndex && this.shouldSkipWhenActive(legs, calculateIndex, activeLegIndex, state, options)) {
                // If we are skipping calculating the path, then we need to udpate the flight path state using the existing leg
                // calculations before returning.
                FlightPathUtils.getLegFinalPosition(calcs, state.currentPosition);
                state.currentCourse = (_b = FlightPathUtils.getLegFinalCourse(calcs)) !== null && _b !== void 0 ? _b : state.currentCourse;
                state.isDiscontinuity = calcs.endsInDiscontinuity;
                state.isFallback = calcs.endsInFallback;
                return calcs;
            }
            try {
                this.calculateMagVar(legs, calculateIndex, activeLegIndex, state, options);
                this.calculateVectors(legs, calculateIndex, activeLegIndex, state, options);
                calcs.endsInDiscontinuity = state.isDiscontinuity;
                calcs.endsInFallback = state.isFallback;
            }
            catch (e) {
                console.error(e);
                if (e instanceof Error) {
                    console.error(e.stack);
                }
                vectors.length = 0;
                calcs.ingress.length = 0;
                calcs.ingressJoinIndex = 0;
                calcs.egress.length = 0;
                calcs.egressJoinIndex = -1;
                calcs.ingressToEgress.length = 0;
                calcs.endsInDiscontinuity = true;
                calcs.endsInFallback = false;
                state.currentPosition.set(NaN, NaN);
                state.currentCourse = undefined;
                state.isDiscontinuity = true;
                state.isFallback = false;
            }
            // Update the start and end lat/lon and initialDtk fields of the leg calculations from the calculated vectors.
            if (vectors.length > 0) {
                const startVector = vectors[0];
                const endVector = vectors[vectors.length - 1];
                calcs.startLat = startVector.startLat;
                calcs.startLon = startVector.startLon;
                calcs.endLat = endVector.endLat;
                calcs.endLon = endVector.endLon;
                const trueDtk = FlightPathUtils.getVectorInitialCourse(startVector);
                if (!isNaN(trueDtk)) {
                    calcs.initialDtk = MagVar.trueToMagnetic(trueDtk, calcs.courseMagVar);
                }
            }
            else {
                calcs.startLat = undefined;
                calcs.startLon = undefined;
                if (state.currentPosition.isValid()) {
                    calcs.endLat = state.currentPosition.lat;
                    calcs.endLon = state.currentPosition.lon;
                }
                else {
                    calcs.endLat = undefined;
                    calcs.endLon = undefined;
                }
                calcs.initialDtk = undefined;
            }
            return calcs;
        }
        /**
         * Checks whether vector calculations should be skipped when the leg to calculate is the active leg.
         * @param legs A sequence of flight plan legs.
         * @param calculateIndex The index of the leg to calculate.
         * @param activeLegIndex The index of the active leg.
         * @param state The current flight path state.
         * @param options Options to use for the leg calculations.
         * @returns Whether to skip vector calculations.
         */
        shouldSkipWhenActive(legs, calculateIndex, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        activeLegIndex, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        state, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        options) {
            return legs[calculateIndex].calculated.flightPath.length > 0;
        }
        /**
         * Calculates the ingress to egress vectors for a flight plan leg and adds them to a leg calculation.
         * @param legCalc The calculations for a flight plan leg.
         */
        resolveIngressToEgress(legCalc) {
            FlightPathUtils.resolveIngressToEgress(legCalc);
        }
    }
    AbstractFlightPathLegCalculator.__geoPointCache = [new GeoPoint(0, 0)];

    /**
     * Builds flight path vectors from simple GeoCircle paths.
     */
    class CircleVectorBuilder {
        /**
         * Builds a flight path vector from a GeoCircle.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param circle The circle which defines the vector path.
         * @param start The start point.
         * @param end The end point.
         * @param flags The flags to set on the vector. Defaults to none (0).
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array, which is always equal to 1.
         */
        build(vectors, index, circle, start, end, flags = 0, heading = null, isHeadingTrue = false) {
            var _a;
            FlightPathUtils.setVectorFromCircle((_a = vectors[index]) !== null && _a !== void 0 ? _a : (vectors[index] = FlightPathUtils.createEmptyVector()), circle, start, end, flags, heading, isHeadingTrue);
            return 1;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        buildGreatCircle(vectors, index, start, arg4, arg5, flags = 0, heading = null, isHeadingTrue = false) {
            if (typeof arg4 === 'number') {
                return this.buildGreatCircleFromPointBearing(vectors, index, start, arg4, arg5, flags, heading, isHeadingTrue);
            }
            else {
                return this.buildGreatCircleFromEndpoints(vectors, index, start, arg4, arg5, flags, heading, isHeadingTrue);
            }
        }
        /**
         * Builds a flight path vector from a great-circle path between two points.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param start The start point.
         * @param end The end point.
         * @param defaultCourse A default course to use to define a path when the start and end points are coincident or
         * antipodal. If the default course is positive, then it will define the path's initial bearing at the start point.
         * If the default course is negative, then its negative will define the path's final bearing at the end point. If not
         * defined, then no vector will be built for coincident or antipodal start and end points.
         * @param flags The flags to set on the vector. Defaults to none (0).
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         */
        buildGreatCircleFromEndpoints(vectors, index, start, end, defaultCourse, flags = 0, heading = null, isHeadingTrue = false) {
            const circle = CircleVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, end);
            if (!circle.isValid()) {
                if (defaultCourse === undefined) {
                    return 0;
                }
                else {
                    if (defaultCourse < 0) {
                        circle.setAsGreatCircle(end, -defaultCourse);
                    }
                    else {
                        circle.setAsGreatCircle(start, defaultCourse);
                    }
                }
            }
            return this.build(vectors, index, circle, start, end, flags, heading, isHeadingTrue);
        }
        /**
         * Builds a flight path vector from a great-circle path originating at a point and initial course and ending after a
         * given distance or at a defined end point.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param start The start point.
         * @param initialCourse The initial true course (bearing) of the path as measured at the start point, in degrees.
         * @param end The parameter that defines the end of the path, as either a distance in great-arc radians or an end
         * point.
         * @param flags The flags to set on the vector. Defaults to none (0).
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         */
        buildGreatCircleFromPointBearing(vectors, index, start, initialCourse, end, flags = 0, heading = null, isHeadingTrue = false) {
            const circle = CircleVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, initialCourse);
            if (typeof end === 'number') {
                end = circle.offsetDistanceAlong(start, end, CircleVectorBuilder.vec3Cache[0], Math.PI);
            }
            return this.build(vectors, index, circle, start, end, flags, heading, isHeadingTrue);
        }
        /**
         * Builds a flight path vector from a path representing a constant-radius turn.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param turnRadius The radius of the turn, in meters.
         * @param turnDirection The direction of the turn.
         * @param center The center of the turn.
         * @param start The start point.
         * @param end The end point.
         * @param flags The flags to set on the vector. Defaults to none (0).
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array, which is always equal to 1.
         */
        buildTurn(vectors, index, turnRadius, turnDirection, center, start, end, flags = 0, heading = null, isHeadingTrue = false) {
            return this.build(vectors, index, FlightPathUtils.getTurnCircle(center, UnitType.METER.convertTo(turnRadius, UnitType.GA_RADIAN), turnDirection, CircleVectorBuilder.geoCircleCache[0]), start, end, flags, heading, isHeadingTrue);
        }
        /**
         * Builds a flight path vector representing a constant-radius turn from a defined start point and initial course
         * toward a defined final course.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param start The start point.
         * @param turnRadius The radius of the turn, in meters.
         * @param turnDirection The direction of the turn.
         * @param fromCourse The initial true course at the start of the turn, in degrees.
         * @param toCourse The final true course at the end of the turn, in degrees.
         * @param flags The flags to set on the vector. Defaults to none (0).
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         */
        buildTurnToCourse(vectors, index, start, turnRadius, turnDirection, fromCourse, toCourse, flags = 0, heading = null, isHeadingTrue = false) {
            if (start instanceof Float64Array) {
                start = CircleVectorBuilder.geoPointCache[0].setFromCartesian(start);
            }
            const radiusRad = UnitType.METER.convertTo(turnRadius, UnitType.GA_RADIAN);
            const turnCenterPoint = CircleVectorBuilder.geoPointCache[1].set(start).offset(fromCourse + (turnDirection === 'left' ? -90 : 90), radiusRad);
            const turnStartBearing = turnCenterPoint.bearingTo(start);
            const turnEndBearing = MathUtils.normalizeAngleDeg(turnStartBearing + (toCourse - fromCourse));
            const turnEndPoint = turnCenterPoint.offset(turnEndBearing, radiusRad, CircleVectorBuilder.geoPointCache[2]);
            if (turnEndPoint.equals(start)) {
                return 0;
            }
            return this.buildTurn(vectors, index, turnRadius, turnDirection, turnCenterPoint, start, turnEndPoint, flags, heading, isHeadingTrue);
        }
    }
    CircleVectorBuilder.vec3Cache = ArrayUtils.create(1, () => Vec3Math.create());
    CircleVectorBuilder.geoPointCache = ArrayUtils.create(3, () => new GeoPoint(0, 0));
    CircleVectorBuilder.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * Builds vectors representing constant-radius turns to join great-circle paths.
     */
    class TurnToJoinGreatCircleVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        build(vectors, index, start, startPathArg, endPath, radius, flags = exports.FlightPathVectorFlags.TurnToCourse, heading = null, isHeadingTrue = false) {
            if (!endPath.isGreatCircle()) {
                throw new Error('TurnToJoinGreatCircleVectorBuilder::build(): end path is not a great circle');
            }
            let startPath;
            if (startPathArg instanceof GeoCircle) {
                if (!startPathArg.isGreatCircle()) {
                    throw new Error('TurnToJoinGreatCircleVectorBuilder::build(): start path is not a great circle');
                }
                else if (!startPathArg.includes(start)) {
                    throw new Error('TurnToJoinGreatCircleVectorBuilder::build(): the starting point does not lie on the starting path.');
                }
                startPath = startPathArg;
            }
            else {
                startPath = TurnToJoinGreatCircleVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, startPathArg);
            }
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, TurnToJoinGreatCircleVectorBuilder.vec3Cache[0]);
            }
            const turnDirection = endPath.encircles(start) ? 'left' : 'right';
            const radiusRad = turnDirection === 'left'
                ? UnitType.METER.convertTo(radius, UnitType.GA_RADIAN)
                : Math.PI - UnitType.METER.convertTo(radius, UnitType.GA_RADIAN);
            const turnStartToCenterNormal = Vec3Math.cross(start, startPath.center, TurnToJoinGreatCircleVectorBuilder.vec3Cache[1]);
            const turnStartToCenterPath = TurnToJoinGreatCircleVectorBuilder.geoCircleCache[1].set(turnStartToCenterNormal, Math.PI / 2);
            const turnCenter = turnStartToCenterPath.offsetDistanceAlong(start, radiusRad, TurnToJoinGreatCircleVectorBuilder.vec3Cache[2]);
            const turnCircle = TurnToJoinGreatCircleVectorBuilder.geoCircleCache[1].set(turnCenter, radiusRad);
            const end = turnCircle.closest(Vec3Math.multScalar(endPath.center, turnDirection === 'left' ? -1 : 1, TurnToJoinGreatCircleVectorBuilder.vec3Cache[3]), TurnToJoinGreatCircleVectorBuilder.vec3Cache[1]);
            return this.circleVectorBuilder.build(vectors, index, turnCircle, start, end, flags, heading, isHeadingTrue);
        }
    }
    TurnToJoinGreatCircleVectorBuilder.vec3Cache = ArrayUtils.create(4, () => Vec3Math.create());
    TurnToJoinGreatCircleVectorBuilder.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * Builds vectors representing paths connecting initial great-circle paths to final great-circle paths via a turn
     * starting at the start point followed by an angled intercept path which intercepts the final path before the end
     * point.
     */
    class InterceptGreatCircleToPointVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.turnToJoinGreatCircleVectorBuilder = new TurnToJoinGreatCircleVectorBuilder();
        }
        /**
         * Builds a sequence of flight path vectors representing a path from a defined start point and initial course which
         * turns and intercepts a final course at a specified angle using a great-circle path at or before a specified end
         * point. Optionally includes a final turn from the intercept path to the final course.
         *
         * If an intercept angle greater than the specified angle is required to intercept the final course before the end
         * point, then no vectors will be built.
         *
         * If the initial and final courses are parallel, then no vectors will be built.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param start The start point.
         * @param startPath A GeoCircle that defines the initial course. Must be a great circle.
         * @param startTurnRadius The radius of the initial turn, in meters.
         * @param startTurnDirection The direction of the initial turn. If not defined, then the direction will be chosen to
         * minimize the angular distance swept by the initial turn.
         * @param interceptAngle The angle at which to intercept the final path, in degrees. Will be clamped to the range
         * `[0, 90]`.
         * @param end The end point. If not defined, then any intercept point on the target path will be considered valid.
         * @param endPath A GeoCircle that defines the final course. Must be a great circle.
         * @param endTurnRadius The radius of the final turn, in meters. If not defined, then vectors will not be built for
         * the final turn.
         * @param startTurnVectorFlags The flags to set on the initial turn vector. Defaults to none (0).
         * @param interceptVectorFlags The flags to set on the intercept path vector. Defaults to none (0).
         * @param endTurnVectorFlags The flags to set on the final turn vector. Defaults to none (0). Ignored if a turn to
         * join the final path is not calculated.
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         * @throws Error if `startPath` or `endPath` is not a great circle.
         */
        build(vectors, index, start, startPath, startTurnRadius, startTurnDirection, interceptAngle, end, endPath, endTurnRadius, startTurnVectorFlags = 0, interceptVectorFlags = 0, endTurnVectorFlags = 0, heading = null, isHeadingTrue = false) {
            if (!startPath.isGreatCircle() || !endPath.isGreatCircle()) {
                throw new Error('InterceptGreatCircleToPointVectorBuilder::build(): start or end path is not a great circle');
            }
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, InterceptGreatCircleToPointVectorBuilder.vec3Cache[0]);
            }
            if (end && !(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, InterceptGreatCircleToPointVectorBuilder.vec3Cache[1]);
            }
            const startToEndPathAngleRad = Vec3Math.unitAngle(startPath.center, endPath.center);
            if (startToEndPathAngleRad <= GeoMath.ANGULAR_TOLERANCE) {
                // The start and end paths are parallel.
                return 0;
            }
            const intersections = InterceptGreatCircleToPointVectorBuilder.intersectionCache;
            const interceptAngleRad = MathUtils.clamp(interceptAngle * Avionics.Utils.DEG2RAD, 0, MathUtils.HALF_PI);
            // The set of centers of great circles that intersect the end path at the desired intercept angle.
            const interceptPathCenters = InterceptGreatCircleToPointVectorBuilder.geoCircleCache[1].set(endPath.center, interceptAngleRad);
            const startTurnRadiusRad = UnitType.METER.convertTo(startTurnRadius, UnitType.GA_RADIAN);
            if (startTurnDirection === undefined) {
                // The direction of the initial turn is not defined, so we have to choosen one ourselves.
                // Calculate the intercept point if the intercept path were to pass through the start point.
                const interceptCount = interceptPathCenters.intersection(InterceptGreatCircleToPointVectorBuilder.geoCircleCache[0].set(start, MathUtils.HALF_PI), intersections);
                if (interceptCount === 0) {
                    // No great-circle path passing through the start point can intercept the final path at the desired intercept
                    // angle. So we will just turn toward the end point if it is defined. If the end point is not defined, then we
                    // will just pick an arbitrary turn direction.
                    startTurnDirection = end && startPath.encircles(end) ? 'left' : 'right';
                }
                else {
                    let intersectionIndex = 0;
                    if (interceptCount > 1) {
                        // There are two great-circle paths passing through the start point that intercept the final path at the
                        // desired angle. One of them will be directed toward the end path and the other will be directed away from
                        // it. We want to choose the one directed toward it.
                        intersectionIndex = endPath.encircles(start) ? 0 : 1;
                    }
                    let cross = Vec3Math.cross(startPath.center, intersections[intersectionIndex], InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]);
                    // sin(x) ~= 0 for x near 0, so to check if the angle between the start path and intercept path is parallel or
                    // antiparallel we can check the magnitude of their cross product without having to call asin().
                    if (Vec3Math.dot(cross, cross) <= GeoMath.ANGULAR_TOLERANCE * GeoMath.ANGULAR_TOLERANCE) {
                        // If the start and intercept paths are parallel or antiparallel, it doesn't matter which direction we turn,
                        // so we will just turn in the direction that aligns us with the end path.
                        cross = Vec3Math.cross(startPath.center, endPath.center, InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]);
                        if (Vec3Math.dot(cross, cross) <= GeoCircle.ANGULAR_TOLERANCE * GeoMath.ANGULAR_TOLERANCE) {
                            // If the start and end paths are antiparallel (they can't be parallel since we would have returned from
                            // the method by now), then we arbitrarily choose to turn right.
                            startTurnDirection = 'right';
                        }
                        else {
                            startTurnDirection = Vec3Math.dot(cross, start) >= 0 ? 'left' : 'right';
                        }
                    }
                    else {
                        startTurnDirection = Vec3Math.dot(cross, start) >= 0 ? 'left' : 'right';
                    }
                }
            }
            const startTurnCircle = FlightPathUtils.getTurnCircleStartingFromPath(start, startPath, startTurnRadiusRad, startTurnDirection, InterceptGreatCircleToPointVectorBuilder.geoCircleCache[0]);
            if (interceptAngleRad <= GeoMath.ANGULAR_TOLERANCE) {
                // If the desired intercept angle is 0 degrees, then the only valid path is when the starting turn ends exactly
                // on the path to intercept and the end of the turn lies before the end point
                if (Math.abs(Vec3Math.unitAngle(startTurnCircle.center, endPath.center) - Math.abs(MathUtils.HALF_PI - startTurnCircle.radius)) > GeoMath.ANGULAR_TOLERANCE) {
                    // The starting turn is not tangent to path to intercept.
                    return 0;
                }
                const startTurnEnd = endPath.closest(startTurnCircle.closest(endPath.center, InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]), InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]);
                const startTurnEndToEndDistance = end ? endPath.angleAlong(startTurnEnd, end, Math.PI) : 0;
                if (startTurnEndToEndDistance < MathUtils.TWO_PI - GeoMath.ANGULAR_TOLERANCE && startTurnEndToEndDistance > Math.PI + GeoMath.ANGULAR_TOLERANCE) {
                    // The end of the starting turn lies after the end point.
                    return 0;
                }
                return this.circleVectorBuilder.build(vectors, index, startTurnCircle, start, startTurnEnd, startTurnVectorFlags, heading, isHeadingTrue);
            }
            // Find the great-circle path that intersects the path to intercept at the desired intercept angle and is tangent
            // to the starting turn.
            // The set of centers of great circles that are tangent to the starting turn.
            const startTurnInterceptTangentCenters = InterceptGreatCircleToPointVectorBuilder.geoCircleCache[2].set(startTurnCircle.center, Math.abs(MathUtils.HALF_PI - startTurnRadiusRad));
            const interceptPathCount = interceptPathCenters.intersection(startTurnInterceptTangentCenters, intersections);
            if (interceptPathCount === 0) {
                return 0;
            }
            const interceptPath = InterceptGreatCircleToPointVectorBuilder.geoCircleCache[1];
            let interceptCrossSign;
            if (startToEndPathAngleRad >= interceptAngleRad) {
                // The starting turn is considered to overshoot if it crosses to the contralateral side of the final path before
                // joining the intercept path that requires the shortest turn to join. The contralateral side is defined as the
                // right side for left turns and the left side for right turns. If this occurs, then we need to choose the second
                // intercept path (if it exists). This is because choosing the first intercept path would trigger a case below
                // that attempts to end the start turn early, which could produce a path that requires the plane to track toward
                // the final path at an angle greater than the intercept angle.
                const overshootThreshold = Math.asin(MathUtils.clamp(Math.cos(interceptAngleRad) * Math.sin(startTurnRadiusRad), 0, 1));
                const doesStartTurnOvershoot = endPath.distance(startTurnCircle.center) > -overshootThreshold + GeoMath.ANGULAR_TOLERANCE;
                interceptPath.set(intersections[interceptPathCount === 1 || !doesStartTurnOvershoot ? 0 : 1], MathUtils.HALF_PI);
                interceptCrossSign = doesStartTurnOvershoot === (startTurnDirection === 'right') ? 1 : -1;
            }
            else {
                // If the start path intersects the final path at a shallower angle than the intercept path, then we always want
                // to choose the intercept path that requires the shortest turn to join. Even if the turn has overshot the final
                // path by the time it can join the chosen intercept path, we handle that case below by attempting to end the
                // start turn early. We are guaranteed that ending the starting turn early will not result in a path that
                // requires the plane to track toward the final path at an angle greater than the intercept angle because if it
                // did, then that would mean the starting turn does not overshoot the final path, which contradicts the initial
                // assumption of ending the start turn early.
                interceptPath.set(intersections[Math.max(1, intersections.length - 1)], MathUtils.HALF_PI);
                interceptCrossSign = startTurnDirection === 'right' ? 1 : -1;
            }
            const startTurnEnd = interceptPath.closest(startTurnCircle.closest(interceptPath.center, InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]), InterceptGreatCircleToPointVectorBuilder.vec3Cache[2]);
            const intercept = Vec3Math.multScalar(Vec3Math.normalize(Vec3Math.cross(interceptPath.center, endPath.center, InterceptGreatCircleToPointVectorBuilder.vec3Cache[3]), InterceptGreatCircleToPointVectorBuilder.vec3Cache[3]), interceptCrossSign, InterceptGreatCircleToPointVectorBuilder.vec3Cache[3]);
            const interceptDistance = interceptPath.distanceAlong(startTurnEnd, intercept, Math.PI, GeoMath.ANGULAR_TOLERANCE);
            const endTurnRadiusRad = endTurnRadius === undefined ? undefined : UnitType.METER.convertTo(endTurnRadius, UnitType.GA_RADIAN);
            // Required turn anticipation for the end turn to join the intercept and final paths.
            let minDInterceptEnd = 0;
            if (endTurnRadiusRad !== undefined) {
                const endTheta = Math.PI - interceptAngleRad;
                const sinMinDInterceptEnd = Math.tan(endTurnRadiusRad) / Math.tan(endTheta / 2);
                if (Math.abs(sinMinDInterceptEnd) <= 1) {
                    minDInterceptEnd = Math.asin(sinMinDInterceptEnd);
                }
            }
            if (end) {
                const interceptToEndDistance = endPath.distanceAlong(intercept, end, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                const interceptToEndOffset = MathUtils.normalizeAngle(interceptToEndDistance, -Math.PI);
                if (interceptToEndOffset < minDInterceptEnd) {
                    // The intercept path does not intercept the final path early enough to make the end turn before the end point.
                    return 0;
                }
            }
            let vectorIndex = index;
            if (interceptDistance < minDInterceptEnd || interceptDistance > Math.PI + GeoMath.ANGULAR_TOLERANCE) {
                // The starting turn ends too late to make a turn to join the final path or the starting turn overshoots the end
                // path before reaching the intercept course -> attempt to end the starting turn early.
                if (endTurnRadiusRad === undefined) {
                    // We don't need to calculate a final turn, so attempt to end the starting turn where it intersects the end path.
                    const startTurnEndPathIntersectionCount = startTurnCircle.intersection(endPath, intersections);
                    if (startTurnEndPathIntersectionCount === 0) {
                        return 0;
                    }
                    const startTurnEndPathIntersection = intersections[startTurnEndPathIntersectionCount === 1 || startTurnDirection === 'right' ? 0 : 1];
                    if (end && endPath.distanceAlong(startTurnEndPathIntersection, end, Math.PI, GeoMath.ANGULAR_TOLERANCE) > Math.PI + GeoMath.ANGULAR_TOLERANCE) {
                        // The starting turn intercepts the final path after the end point
                        return 0;
                    }
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startTurnCircle, start, startTurnEndPathIntersection, startTurnVectorFlags, heading, isHeadingTrue);
                }
                else {
                    // We need to calculate a final turn to join the end path. This final turn must be tangent to the starting turn
                    // and the end path.
                    const startTurnCenter = FlightPathUtils.getTurnCenterFromCircle(startTurnCircle, InterceptGreatCircleToPointVectorBuilder.vec3Cache[4]);
                    // The set of centers of all geo circles of the desired end turn radius that are tangent to the starting turn.
                    const startTurnEndTurnTangentCenters = InterceptGreatCircleToPointVectorBuilder.geoCircleCache[1].set(startTurnCenter, startTurnRadiusRad + endTurnRadiusRad);
                    // The set of centers of all geo circles of the desired end turn radius that are tangent to the end path.
                    const endPathEndTurnTangentCenters = InterceptGreatCircleToPointVectorBuilder.geoCircleCache[2].set(endPath.center, endPath.radius + endTurnRadiusRad * (startTurnDirection === 'left' ? 1 : -1));
                    const endTurnCircleCount = endPathEndTurnTangentCenters.intersection(startTurnEndTurnTangentCenters, intersections);
                    if (endTurnCircleCount === 0) {
                        return 0;
                    }
                    const endTurnCenter = intersections[0];
                    const endTurnCircle = FlightPathUtils.getTurnCircle(endTurnCenter, endTurnRadiusRad, startTurnDirection === 'left' ? 'right' : 'left', InterceptGreatCircleToPointVectorBuilder.geoCircleCache[1]);
                    endTurnCircle.closest(startTurnCenter, startTurnEnd);
                    const endTurnEnd = endPath.closest(endTurnCenter, InterceptGreatCircleToPointVectorBuilder.vec3Cache[4]);
                    if (end && endPath.distanceAlong(endTurnEnd, end, Math.PI, GeoMath.ANGULAR_TOLERANCE) > Math.PI + GeoMath.ANGULAR_TOLERANCE) {
                        // The ending turn joins the final path after the end point.
                        return 0;
                    }
                    // Starting turn.
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startTurnCircle, start, startTurnEnd, startTurnVectorFlags, heading, isHeadingTrue);
                    // Ending turn.
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, endTurnCircle, startTurnEnd, endTurnEnd, endTurnVectorFlags, heading, isHeadingTrue);
                }
            }
            else {
                // The starting turn neither overshoots the end path before joining the intercept path nor ends too late to make
                // a turn to join the end path before the end point.
                // Starting turn.
                if (Vec3Math.unitAngle(start, startTurnEnd) > GeoMath.ANGULAR_TOLERANCE) {
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startTurnCircle, start, startTurnEnd, startTurnVectorFlags, heading, isHeadingTrue);
                }
                const interceptPathEnd = endTurnRadiusRad === undefined
                    ? intercept
                    : interceptPath.offsetDistanceAlong(intercept, -minDInterceptEnd, InterceptGreatCircleToPointVectorBuilder.vec3Cache[4], Math.PI);
                // Intercept path.
                if (interceptDistance - minDInterceptEnd > GeoMath.ANGULAR_TOLERANCE) {
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, interceptPath, startTurnEnd, interceptPathEnd, interceptVectorFlags, heading, isHeadingTrue);
                }
                // Ending turn.
                if (endTurnRadius !== undefined) {
                    vectorIndex += this.turnToJoinGreatCircleVectorBuilder.build(vectors, vectorIndex, interceptPathEnd, interceptPath, endPath, endTurnRadius, endTurnVectorFlags, heading, isHeadingTrue);
                }
            }
            return vectorIndex - index;
        }
    }
    InterceptGreatCircleToPointVectorBuilder.vec3Cache = ArrayUtils.create(5, () => Vec3Math.create());
    InterceptGreatCircleToPointVectorBuilder.geoCircleCache = ArrayUtils.create(3, () => new GeoCircle(Vec3Math.create(), 0));
    InterceptGreatCircleToPointVectorBuilder.intersectionCache = [Vec3Math.create(), Vec3Math.create()];

    /**
     * Builds vectors representing procedure turn paths.
     */
    class ProcedureTurnVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        /**
         * Builds a sequence of vectors representing a procedure turn from a defined starting point and initial course to a
         * defined end point and final course. A procedure turn begins with a variable-length leg from the start point along
         * the initial course followed by an initial turn to intercept the outbound leg of the procedure turn, then a
         * variable-length outbound leg, a 180-degree turn, a variable-length inbound leg, and finally a turn to intercept
         * the final course at the end point. If a full set of vectors cannot be computed given the restraints imposed by the
         * path geometry and the desired turn radius, parts of the turn beginning with the inbound leg of the procedure turn
         * may be altered or omitted entirely.
         * @param vectors The flight path vector sequence to which to add the vectors.
         * @param index The index in the sequence at which to add the vectors.
         * @param start The start point.
         * @param startPath A GeoCircle that defines the initial course. Must be a great circle.
         * @param end The end point.
         * @param endPath A GeoCircle that defines the final course. Must be a great circle.
         * @param outboundCourse The true course, in degrees, of the outbound leg of the turn.
         * @param desiredTurnRadius The desired turn radius, in meters.
         * @param desiredTurnDirection The desired turn direction.
         * @param initialCourse The initial course. If not defined, it will be calculated from `startPath` and `start`.
         * @param finalCourse The final course. If not defined, it will be calculated from `endPath` and `end`.
         * @param flags The flags to set on all built vectors. Defaults to the `CourseReversal` flag.
         * @param includeTurnToCourseFlag Whether to include the `TurnToCourse` flag on the turn vectors. Defaults to `true`.
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         * @throws Error if either the start or end path is not a great circle.
         */
        build(vectors, index, start, startPath, end, endPath, outboundCourse, desiredTurnRadius, desiredTurnDirection, initialCourse, finalCourse, flags = exports.FlightPathVectorFlags.CourseReversal, includeTurnToCourseFlag = true, heading = null, isHeadingTrue = false) {
            var _a, _b;
            if (!startPath.isGreatCircle() || !endPath.isGreatCircle()) {
                throw new Error('ProcedureTurnVectorBuilder::build(): start or end path is not a great circle');
            }
            let vectorIndex = index;
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, ProcedureTurnVectorBuilder.vec3Cache[0]);
            }
            if (!(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, ProcedureTurnVectorBuilder.vec3Cache[1]);
            }
            initialCourse !== null && initialCourse !== void 0 ? initialCourse : (initialCourse = startPath.bearingAt(start, Math.PI));
            finalCourse !== null && finalCourse !== void 0 ? finalCourse : (finalCourse = endPath.bearingAt(end, Math.PI));
            // We need to calculate two parameters:
            // (1) The distance to stay on the leg on the initial course.
            // (2) The distance to stay on the outbound leg.
            // We ideally would like to choose these parameters such that the 180-degree turn after the outbound leg ends at a
            // location where it can immediately make another turn to intercept the final course (i.e. reduce the distance of
            // the inbound leg to 0). However, this may not be possible since we are constrained by the fact that the two
            // distance parameters cannot be negative. To simplify the math, we will do an approximate calculation based on a
            // pseudo-Euclidean geometry instead of spherical geometry. The error of the approximation is proportional to how
            // much the angle between the initial and final courses deviate from 180 degrees; if they are exactly antiparallel
            // then the error is zero.
            const startPoint = ProcedureTurnVectorBuilder.geoPointCache[0].setFromCartesian(start);
            const initialTurnDirection = (_a = FlightPathUtils.getShortestTurnDirection(initialCourse, outboundCourse)) !== null && _a !== void 0 ? _a : 'right';
            const initialTurnDirectionSign = initialTurnDirection === 'left' ? -1 : 1;
            const startPathEndPointDistance = startPath.distance(end);
            const isInitialTurnTowardEndPath = (startPathEndPointDistance <= GeoMath.ANGULAR_TOLERANCE) === (initialTurnDirection === 'left');
            const deltaOutbound = Math.abs(MathUtils.angularDistanceDeg(initialCourse, outboundCourse, 0)) * Avionics.Utils.DEG2RAD;
            const thetaOutbound = (Math.PI - deltaOutbound) / 2;
            const desiredTurnRadiusRad = UnitType.METER.convertTo(desiredTurnRadius, UnitType.GA_RADIAN);
            // If there is a desired turn direction, honor it. Otherwise choose the direction that results in the shortest path
            // to intercept the next leg.
            const turnDirection = desiredTurnDirection !== null && desiredTurnDirection !== void 0 ? desiredTurnDirection : (((initialTurnDirection === 'left') === deltaOutbound < Math.PI) ? 'right' : 'left');
            const turnDirectionSign = turnDirection === 'left' ? -1 : 1;
            const endPointToStartPathXTrackDistance = Math.abs(startPathEndPointDistance);
            let desiredAlongTurnOutboundPathDistance = Math.abs(deltaOutbound - Math.PI / 2) > 1e-10
                ? Math.asin(MathUtils.clamp(Math.tan(2 * desiredTurnRadiusRad) / Math.tan(deltaOutbound), -1, 1)) * (turnDirection === initialTurnDirection ? -1 : 1)
                : 0;
            desiredAlongTurnOutboundPathDistance += Math.asin(MathUtils.clamp(Math.sin(endPointToStartPathXTrackDistance) / Math.sin(deltaOutbound), -1, 1))
                * (isInitialTurnTowardEndPath ? 1 : -1);
            const alongTurnOutboundPathDistance = Math.max(0, desiredAlongTurnOutboundPathDistance);
            let desiredAlongStartPathDistance = MathUtils.normalizeAngle(startPath.distanceAlong(start, end, Math.PI), -Math.PI);
            desiredAlongStartPathDistance -= desiredAlongTurnOutboundPathDistance === 0
                ? 0
                : Math.atan(Math.cos(deltaOutbound) * Math.tan(desiredAlongTurnOutboundPathDistance));
            desiredAlongStartPathDistance += Math.asin(MathUtils.clamp(Math.sin(deltaOutbound) * Math.sin(2 * desiredTurnRadiusRad), -1, 1))
                * (turnDirection === initialTurnDirection ? 1 : -1);
            const alongStartPathDistance = Math.max(0, desiredAlongStartPathDistance);
            const initialTurnStartPoint = alongStartPathDistance > 0
                ? startPath.offsetDistanceAlong(start, alongStartPathDistance, ProcedureTurnVectorBuilder.geoPointCache[1])
                : startPoint;
            const initialTurnCenterPoint = initialTurnStartPoint.offset(initialCourse + 90 * initialTurnDirectionSign, desiredTurnRadiusRad, ProcedureTurnVectorBuilder.geoPointCache[2]);
            const initialTurnHalfAngularWidth = Math.acos(Math.sin(thetaOutbound) * Math.cos(desiredTurnRadiusRad)) * Avionics.Utils.RAD2DEG;
            const initialTurnStartBearing = initialTurnCenterPoint.bearingTo(initialTurnStartPoint);
            const initialTurnEndBearing = MathUtils.normalizeAngleDeg(initialTurnStartBearing + initialTurnHalfAngularWidth * 2 * initialTurnDirectionSign);
            const initialTurnEndPoint = initialTurnCenterPoint.offset(initialTurnEndBearing, desiredTurnRadiusRad, ProcedureTurnVectorBuilder.geoPointCache[3]);
            const turnStartPoint = alongTurnOutboundPathDistance > 0
                ? initialTurnEndPoint.offset(outboundCourse, alongTurnOutboundPathDistance, ProcedureTurnVectorBuilder.geoPointCache[4])
                : initialTurnEndPoint;
            const turnCenterPoint = turnStartPoint.offset(outboundCourse + 90 * turnDirectionSign, desiredTurnRadiusRad, ProcedureTurnVectorBuilder.geoPointCache[5]);
            const turnStartBearing = turnCenterPoint.bearingTo(turnStartPoint);
            let turnEndBearing = (turnStartBearing + 180) % 360;
            const turnEndPoint = turnCenterPoint.offset(turnEndBearing, desiredTurnRadiusRad, ProcedureTurnVectorBuilder.geoPointCache[6]);
            const turnEndVec = turnEndPoint.toCartesian(ProcedureTurnVectorBuilder.vec3Cache[2]);
            let finalTurnDirection;
            let finalTurnRadius;
            let finalTurnStartPoint;
            let finalTurnCenterPoint;
            let finalTurnEndPoint;
            let endPoint;
            if (endPath.encircles(turnEndVec) === (initialTurnDirection === 'left')) {
                // The end of the 180-degree turn from the outbound leg to the inbound leg lies beyond the final course due to
                // approximation error, so we need to end the turn early.
                const turnCircle = ProcedureTurnVectorBuilder.geoCircleCache[0].set(turnCenterPoint, desiredTurnRadiusRad);
                const intersections = ProcedureTurnVectorBuilder.intersectionCache;
                const numIntersections = turnCircle.intersection(endPath, intersections);
                if (numIntersections === 0) {
                    // The final course path is completely outside of the turn, which can only happen if there is a major deviation
                    // from the pseudo-Euclidean approximation. There is no easy way to recover from this state, so we will just
                    // bail out and connect the end of the inbound leg to the end point with a great-circle path.
                    endPoint = ProcedureTurnVectorBuilder.geoPointCache[7].setFromCartesian(end);
                }
                else {
                    if (numIntersections === 2) {
                        // Choose the intersection that gives the smallest angle between the end of the turn and the final course path.
                        const headingAdjustment = MathUtils.HALF_PI * turnDirectionSign;
                        const angleDiff_0 = MathUtils.angularDistance(Vec3Math.unitAngle(GeoCircle.getGreatCircleNormal(turnCenterPoint, intersections[0], ProcedureTurnVectorBuilder.vec3Cache[3]), endPath.center) + headingAdjustment, 0, 0);
                        const angleDiff_1 = MathUtils.angularDistance(Vec3Math.unitAngle(GeoCircle.getGreatCircleNormal(turnCenterPoint, intersections[1], ProcedureTurnVectorBuilder.vec3Cache[3]), endPath.center) + headingAdjustment, 0, 0);
                        turnEndPoint.setFromCartesian(intersections[angleDiff_0 < angleDiff_1 ? 0 : 1]);
                    }
                    else {
                        turnEndPoint.setFromCartesian(intersections[0]);
                    }
                    turnEndBearing = turnCenterPoint.bearingTo(turnEndPoint);
                }
            }
            else {
                const turnFinalCourse = MathUtils.normalizeAngleDeg(outboundCourse + 180);
                const turnInboundPath = ProcedureTurnVectorBuilder.geoCircleCache[0].setAsGreatCircle(turnEndPoint, turnFinalCourse);
                const intersections = ProcedureTurnVectorBuilder.intersectionCache;
                const numIntersections = turnInboundPath.intersection(endPath, intersections);
                // Only move forward if the end of the turn does not lie on the final course path.
                const endPathTurnEndDistance = endPath.distance(turnEndVec);
                if (numIntersections !== 0 && Math.abs(endPathTurnEndDistance) > GeoMath.ANGULAR_TOLERANCE) {
                    const intersection = intersections[(numIntersections === 1 || endPathTurnEndDistance <= GeoMath.ANGULAR_TOLERANCE) ? 0 : 1];
                    // Only move forward if the intersection lies before the endpoint, otherwise we will just end the leg at the
                    // end of the 180-degree turn.
                    if (Vec3Math.dot(GeoCircle.getGreatCircleNormal(intersection, end, ProcedureTurnVectorBuilder.vec3Cache[3]), endPath.center) > 0) {
                        // Because we used an approximation to place the procedure turn, the inbound leg may not allow for a
                        // turn of the desired radius to perfectly intercept the final path. Therefore, we need to explicitly
                        // calculate the maximum allowed turn radius for this final turn and adjust the turn radius as needed.
                        // Note that if the initial and final paths are antiparallel, these calculations are not strictly
                        // necessary, but we will carry them out in all cases to account for floating point errors that may
                        // have accumulated during previous calculations.
                        const deltaInbound = Vec3Math.unitAngle(endPath.center, turnInboundPath.center);
                        const thetaInbound = (Math.PI - deltaInbound) / 2;
                        const tanThetaInbound = Math.tan(thetaInbound);
                        const desiredFinalTurnAlongTrackDistance = Math.asin(MathUtils.clamp(Math.tan(desiredTurnRadiusRad) / tanThetaInbound, -1, 1));
                        const finalTurnAlongTrackDistance = Math.min(desiredFinalTurnAlongTrackDistance, Vec3Math.unitAngle(intersection, turnEndVec), Vec3Math.unitAngle(intersection, end));
                        const finalTurnRadiusRad = finalTurnAlongTrackDistance === desiredFinalTurnAlongTrackDistance
                            ? desiredTurnRadiusRad
                            : Math.atan(Math.sin(finalTurnAlongTrackDistance) * tanThetaInbound);
                        finalTurnDirection = (_b = FlightPathUtils.getShortestTurnDirection(turnFinalCourse, finalCourse)) !== null && _b !== void 0 ? _b : 'right';
                        finalTurnRadius = UnitType.GA_RADIAN.convertTo(finalTurnRadiusRad, UnitType.METER);
                        finalTurnStartPoint = turnInboundPath.offsetDistanceAlong(intersection, -finalTurnAlongTrackDistance, ProcedureTurnVectorBuilder.geoPointCache[7], Math.PI);
                        finalTurnCenterPoint = finalTurnStartPoint.offset(turnFinalCourse + (finalTurnDirection === 'left' ? -90 : 90), finalTurnRadiusRad, ProcedureTurnVectorBuilder.geoPointCache[8]);
                        finalTurnEndPoint = endPath.offsetDistanceAlong(intersection, finalTurnAlongTrackDistance, ProcedureTurnVectorBuilder.geoPointCache[9], Math.PI);
                    }
                }
            }
            // Set vectors.
            const turnFlags = flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : 0);
            // Start point to start of turn onto outbound leg.
            if (initialTurnStartPoint !== startPoint) {
                vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, startPoint, initialTurnStartPoint, initialCourse, flags, heading, isHeadingTrue);
            }
            // Turn onto outbound leg.
            vectorIndex += this.circleVectorBuilder.buildTurn(vectors, vectorIndex, desiredTurnRadius, initialTurnDirection, initialTurnCenterPoint, initialTurnStartPoint, initialTurnEndPoint, turnFlags, heading, isHeadingTrue);
            // Outbound leg.
            if (turnStartPoint !== initialTurnEndPoint) {
                vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, initialTurnEndPoint, turnStartPoint, outboundCourse, flags, heading, isHeadingTrue);
            }
            // 180-degree turn from outbound leg to inbound leg.
            vectorIndex += this.circleVectorBuilder.buildTurn(vectors, vectorIndex, desiredTurnRadius, turnDirection, turnCenterPoint, turnStartPoint, turnEndPoint, turnFlags, heading, isHeadingTrue);
            if (finalTurnCenterPoint) {
                // At this point we are guaranteed that finalTurnStartPoint, finalTurnEndPoint, and finalTurnDirection are all defined.
                // Inbound leg.
                if (!finalTurnStartPoint.equals(turnEndPoint)) {
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, turnEndPoint, finalTurnStartPoint, outboundCourse + 180, flags, heading, isHeadingTrue);
                }
                // Turn onto final course.
                vectorIndex += this.circleVectorBuilder.buildTurn(vectors, vectorIndex, finalTurnRadius, finalTurnDirection, finalTurnCenterPoint, finalTurnStartPoint, finalTurnEndPoint, turnFlags, heading, isHeadingTrue);
            }
            else {
                if (endPoint) {
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, turnEndPoint, endPoint, -finalCourse, flags, heading, isHeadingTrue);
                }
            }
            return vectorIndex - index;
        }
    }
    ProcedureTurnVectorBuilder.vec3Cache = ArrayUtils.create(4, () => Vec3Math.create());
    ProcedureTurnVectorBuilder.geoPointCache = ArrayUtils.create(10, () => new GeoPoint(0, 0));
    ProcedureTurnVectorBuilder.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
    ProcedureTurnVectorBuilder.intersectionCache = [Vec3Math.create(), Vec3Math.create()];

    /**
     * Calculates flight path vectors for legs that terminate at a target altitude.
     */
    class AltitudeLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of AltitudeLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.__vec3Cache = ArrayUtils.create(2, () => Vec3Math.create());
            this.__geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
            this.__geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.interceptGreatCircleToPointVectorBuilder = new InterceptGreatCircleToPointVectorBuilder();
        }
        /**
         * Builds a flight path vector representing the path the airplane must fly to climb to the target altitude of a
         * flight plan leg.
         * @param vectors The flight path vector array to which to add the vector.
         * @param index The index in the array at which to add the vector.
         * @param leg The flight plan leg for which the vector is to be built.
         * @param isActiveLeg Whether the vector is to be built for the active flight plan leg.
         * @param state The current flight path state.
         * @param path A GeoCircle that defines the path of the vector to build.
         * @param start The start point of the vector to build.
         * @param flags The flags to set on the vector.
         * @param heading The heading-to-fly to assign to the vector, in degrees, or `null` if no heading is to be assigned.
         * Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to the vector is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         */
        buildDistanceToAltitudeVector(vectors, index, leg, isActiveLeg, state, path, start, flags, heading = null, isHeadingTrue = false) {
            const deltaAltitude = Math.max(UnitType.METER.convertTo(leg.leg.altitude1, UnitType.FOOT) - state.planeAltitude.asUnit(UnitType.FOOT), 0);
            const distanceToClimb = UnitType.NMILE.convertTo(deltaAltitude / state.planeClimbRate.asUnit(UnitType.FPM) / 60 * state.planeSpeed.asUnit(UnitType.KNOT), UnitType.GA_RADIAN);
            const climbStartVec = isActiveLeg && state.planePosition.isValid() ? path.closest(state.planePosition, this.__vec3Cache[0]) : start;
            const originToClimbStartDistance = MathUtils.normalizeAngle(path.distanceAlong(start, climbStartVec, Math.PI, GeoMath.ANGULAR_TOLERANCE), -Math.PI);
            // TODO: make minimum distance configurable
            const minDistance = isActiveLeg
                ? UnitType.FOOT.convertTo(100, UnitType.GA_RADIAN)
                : UnitType.NMILE.convertTo(0.1, UnitType.GA_RADIAN);
            const offsetDistance = Math.max(originToClimbStartDistance + distanceToClimb, minDistance);
            const endVec = path.offsetDistanceAlong(start, offsetDistance, this.__vec3Cache[0], Math.PI);
            return this.circleVectorBuilder.build(vectors, index, path, start, endVec, flags, heading, isHeadingTrue);
        }
        /**
         * Builds a sequence of flight path vectors representing a path that intercepts a desired great-circle path. The
         * intercept path begins at the flight path state's current position and course. If an intercept path can be
         * calculated, then it is guaranteed to end at or past a specified origin point along the path to intercept. If an
         * intercept path cannot be calculated, then vectors will be built that represent a constant-radius turn toward the
         * path to intercept's initial course.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param isActiveLeg Whether the vectors are to be built for the active flight plan leg.
         * @param state The current flight path state. If an intercept is successfully calculated, then the state's fallback
         * flag will be set to false. If an intercept could not be calculated, then the fallback flag will not be changed.
         * @param pathToInterceptCourse The initial true course of the path to intercept, in degrees.
         * @param path A GeoCircle that defines the path to intercept. If an intercept cannot be calculated, then the circle
         * will be changed to the great-circle that defines the course along which the last calculated fallback vector ends.
         * @param origin The origin point of the path to intercept. The point will be changed, if necessary, to the point at
         * which the built vectors actually intercept the path if the vectors intercept the path past the original origin
         * point (as measured along the path to intercept).
         * @returns The number of vectors that were built and added to the array.
         */
        buildFallbackVectorsToInterceptPath(vectors, index, isActiveLeg, state, pathToInterceptCourse, path, origin) {
            var _a;
            let vectorIndex = index;
            const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None)
                | (state.isFallback && state.currentCourse !== undefined ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None);
            if (isActiveLeg && vectors.length > 0 && BitFlags.isAll(vectors[0].flags, exports.FlightPathVectorFlags.Fallback)) {
                // If this is the active leg and fallback vectors have already been calculated, then preserve the previously
                // calculated vectors except for the last one, which is guaranteed to be a great-circle vector along the final
                // path that is supposed to cover the distance required to climb to the target altitude.
                vectorIndex = vectors.length - 1;
                const prevVector = vectors[vectorIndex - 1];
                state.currentPosition.set(prevVector.endLat, prevVector.endLon);
                state.currentCourse = pathToInterceptCourse;
                if (BitFlags.isAll(vectors[vectorIndex].flags, exports.FlightPathVectorFlags.Fallback)) {
                    state.currentPosition.toCartesian(origin);
                    path.setAsGreatCircle(state.currentPosition, pathToInterceptCourse);
                }
            }
            else {
                // If the current course is not defined, then set it to the course from the current position to the leg origin.
                if (state.currentCourse === undefined) {
                    state.currentCourse = state.currentPosition.bearingTo(this.__geoPointCache[0].setFromCartesian(origin));
                    // If the current position is coincident with or antipodal to the leg origin, then set the current course to
                    // the leg course.
                    if (isNaN(state.currentCourse)) {
                        state.currentCourse = path.bearingAt(origin);
                    }
                }
                const initialPath = this.__geoCircleCache[0].setAsGreatCircle(state.currentPosition, state.currentCourse);
                const initialFinalPathAngle = Vec3Math.unitAngle(initialPath.center, path.center);
                if (initialFinalPathAngle >= Math.PI - GeoMath.ANGULAR_TOLERANCE) {
                    // If the initial path and the final path are antiparallel, then we will path a procedure turn to do a 180.
                    vectorIndex += this.procTurnVectorBuilder.build(vectors, vectorIndex, state.currentPosition, initialPath, origin, path, state.currentCourse + 45, state.desiredCourseReversalTurnRadius.asUnit(UnitType.METER), undefined, state.currentCourse, pathToInterceptCourse, flags | exports.FlightPathVectorFlags.CourseReversal, true);
                }
                else if (initialFinalPathAngle > GeoMath.ANGULAR_TOLERANCE) {
                    const interceptFlags = flags | exports.FlightPathVectorFlags.InterceptCourse;
                    const turnFlags = interceptFlags | exports.FlightPathVectorFlags.TurnToCourse;
                    vectorIndex += this.interceptGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, initialPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, 45, path.offsetAngleAlong(origin, MathUtils.HALF_PI, this.__vec3Cache[0], Math.PI), path, state.desiredTurnRadius.asUnit(UnitType.METER), turnFlags, interceptFlags, turnFlags);
                }
                let isOnFinalPath = false;
                if (initialFinalPathAngle <= GeoMath.ANGULAR_TOLERANCE) {
                    // We were already on the final path at the start of the leg.
                    isOnFinalPath = true;
                }
                else if (vectorIndex > 0) {
                    const lastVector = vectors[vectorIndex - 1];
                    state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                    state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
                    // To check if we are on the final path, first confirm that the last vector ends on the final path. Then,
                    // confirm that the great circle tangent to the last vector where the vector ends is parallel to the final
                    // path.
                    const lastVectorEndVec = state.currentPosition.toCartesian(this.__vec3Cache[0]);
                    if (path.includes(lastVectorEndVec)) {
                        const tangentCircleNormal = Vec3Math.normalize(Vec3Math.cross(Vec3Math.cross(lastVectorEndVec, Vec3Math.set(lastVector.centerX, lastVector.centerY, lastVector.centerZ, this.__vec3Cache[1]), this.__vec3Cache[1]), lastVectorEndVec, this.__vec3Cache[1]), this.__vec3Cache[1]);
                        // Angular difference <= 1e-6 radians
                        isOnFinalPath = Vec3Math.dot(tangentCircleNormal, path.center) >= 0.9999999999995;
                    }
                }
                if (isOnFinalPath) {
                    // If we are on the final path, then check if we are behind the origin position. If we are, then we need to
                    // ensure we travel along the final path to the origin.
                    const startVec = state.currentPosition.toCartesian(this.__vec3Cache[0]);
                    const distanceToOrigin = path.distanceAlong(startVec, origin, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                    if (distanceToOrigin < Math.PI) {
                        if (distanceToOrigin > 0) {
                            vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, path, startVec, origin);
                        }
                    }
                    else {
                        Vec3Math.copy(startVec, origin);
                    }
                    state.isFallback = false;
                }
                else {
                    // If we are not on the final path, then we will just turn toward the prescribed course and fly for the desired
                    // distance along that course.
                    if (MathUtils.angularDistanceDeg(state.currentCourse, pathToInterceptCourse, 0) > 1) {
                        vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), (_a = FlightPathUtils.getShortestTurnDirection(state.currentCourse, pathToInterceptCourse)) !== null && _a !== void 0 ? _a : 'right', state.currentCourse, pathToInterceptCourse, exports.FlightPathVectorFlags.Fallback | exports.FlightPathVectorFlags.TurnToCourse);
                        if (vectorIndex > 0) {
                            const lastVector = vectors[vectorIndex - 1];
                            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                        }
                        state.currentCourse = pathToInterceptCourse;
                    }
                    state.currentPosition.toCartesian(origin);
                    path.setAsGreatCircle(state.currentPosition, pathToInterceptCourse);
                }
            }
            return vectorIndex - index;
        }
    }
    /**
     * Calculates flight path vectors for course to altitude legs.
     */
    class CourseToAltitudeLegCalculator extends AltitudeLegCalculator {
        /**
         * Creates a new instance of CourseToAltitudeLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache);
            this.vec3Cache = ArrayUtils.create(1, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined && !state.isDiscontinuity && state.currentPosition.isValid()) {
                magVar = MagVar.get(state.currentPosition);
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            var _a, _b;
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            // TODO: not sure if course-to... legs are allowed to have floating origins based on current airplane position.
            // Currently this calculator does not allow floating origins based on airplane position (the leg must begin at and
            // be continuous with the end of the previous leg).
            if (state.isDiscontinuity || !state.currentPosition.isValid()) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const isActiveLeg = calculateIndex === activeLegIndex;
            let vectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const finalPath = this.geoCircleCache[0];
            const originVec = this.vec3Cache[0];
            let didCalculateFallback = false;
            // If the leg begins in a fallback state, then attempt to get the intended terminator position of the previous leg
            // and build a path to intercept the desired course from the intended terminator position.
            if (state.isFallback && state.currentCourse !== undefined && calculateIndex > 0) {
                const prevLegTerminatorPos = this.getTerminatorPosition(legs[calculateIndex - 1].leg, this.geoPointCache[0]);
                if (prevLegTerminatorPos) {
                    finalPath.setAsGreatCircle(prevLegTerminatorPos, course);
                    prevLegTerminatorPos.toCartesian(originVec);
                    vectorIndex += this.buildFallbackVectorsToInterceptPath(vectors, vectorIndex, isActiveLeg, state, course, finalPath, originVec);
                    didCalculateFallback = true;
                }
            }
            if (!didCalculateFallback) {
                // If we didn't calculate a fallback path, then calculate an initial turn toward the desired course. The reason
                // we don't need to do this when a fallback path is calculated is because the fallback path is guaranteed to
                // end on the desired course.
                if (state.currentCourse !== undefined && MathUtils.angularDistanceDeg(state.currentCourse, course, 0) > 1) {
                    const turnDirection = (_b = (_a = FlightPathUtils.getLegDesiredTurnDirection(leg.leg)) !== null && _a !== void 0 ? _a : FlightPathUtils.getShortestTurnDirection(state.currentCourse, course)) !== null && _b !== void 0 ? _b : 'right';
                    vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), turnDirection, state.currentCourse, course, exports.FlightPathVectorFlags.TurnToCourse);
                    if (vectorIndex > 0) {
                        const lastVector = vectors[vectorIndex - 1];
                        state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                        state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
                    }
                }
                finalPath.setAsGreatCircle(state.currentPosition, course);
                state.currentPosition.toCartesian(originVec);
            }
            vectorIndex += this.buildDistanceToAltitudeVector(vectors, vectorIndex, leg, isActiveLeg, state, finalPath, originVec, state.isFallback ? exports.FlightPathVectorFlags.Fallback : 0);
            vectors.length = vectorIndex;
            // NOTE: the vector array cannot be empty because buildDistanceToAltitudeVector() always builds one vector.
            const lastVector = vectors[vectorIndex - 1];
            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }
    /**
     * Calculates flight path vectors for fix to altitude legs.
     */
    class FixToAltitudeLegCalculator extends AltitudeLegCalculator {
        /**
         * Creates a new instance of FixToAltitudeLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache);
            this.vec3Cache = ArrayUtils.create(1, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            leg.calculated.courseMagVar = originPos === undefined ? 0 : this.getLegMagVar(leg.leg, originPos);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            if (!originPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const isActiveLeg = calculateIndex === activeLegIndex;
            let vectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const finalPath = this.geoCircleCache[0].setAsGreatCircle(originPos, course);
            const originVec = originPos.toCartesian(this.vec3Cache[0]);
            // If the leg begins in a discontinuity and we are configured to calculate discontinuity vectors or if the leg
            // begins in a fallback state, then build a path to intercept the desired course. Otherwise, start the leg from the
            // defined origin.
            if (state.currentPosition.isValid()
                && (!state.isDiscontinuity || options.calculateDiscontinuityVectors)
                && (state.isDiscontinuity || (state.isFallback && state.currentCourse !== undefined))) {
                vectorIndex += this.buildFallbackVectorsToInterceptPath(vectors, vectorIndex, isActiveLeg, state, course, finalPath, originVec);
            }
            else {
                state.currentPosition.set(originPos);
                state.currentCourse = course;
            }
            vectorIndex += this.buildDistanceToAltitudeVector(vectors, vectorIndex, leg, isActiveLeg, state, finalPath, originVec, state.isFallback ? exports.FlightPathVectorFlags.Fallback : 0);
            vectors.length = vectorIndex;
            // NOTE: the vector array cannot be empty because buildDistanceToAltitudeVector() always builds one vector.
            const lastVector = vectors[vectorIndex - 1];
            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }
    /**
     * Calculates flight path vectors for heading to altitude legs.
     */
    class HeadingToAltitudeLegCalculator extends AltitudeLegCalculator {
        /**
         * Creates a new instance of HeadingToAltitudeLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache);
            this.vec3Cache = ArrayUtils.create(1, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined) {
                let position;
                if (calculateIndex === activeLegIndex && state.planePosition.isValid()) {
                    position = state.planePosition;
                }
                else if (!state.isDiscontinuity && state.currentPosition.isValid()) {
                    position = state.currentPosition;
                }
                if (position) {
                    magVar = MagVar.get(position);
                }
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            var _a, _b;
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const heading = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const isHeadingTrue = leg.leg.trueDegrees;
            let course = heading;
            if (state.planeWindSpeed.number > 0) {
                course = NavMath.headingToGroundTrack(heading, state.planeTrueAirspeed.asUnit(UnitType.KNOT), state.planeWindDirection, state.planeWindSpeed.asUnit(UnitType.KNOT));
                if (isNaN(course)) {
                    course = heading;
                }
            }
            let startAtPlanePos = false;
            let retainOldVectors = false;
            const isActiveLeg = calculateIndex === activeLegIndex;
            if (isActiveLeg && state.planePosition.isValid()) {
                // If the leg to calculate is the active leg and we know the airplane's current position, then we should start
                // the leg path at the airplane's current position. The exception to this is if vectors have been previously
                // calculated and include an initial turn and the airplane's current position puts it within a certain
                // cross-track distance of the turn and before the end of the turn. In this case, we will retain the previously
                // calculated vectors.
                startAtPlanePos = true;
                if (vectors.length > 0) {
                    const firstVector = vectors[0];
                    if (!FlightPathUtils.isVectorGreatCircle(firstVector)) {
                        const firstVectorCircle = FlightPathUtils.setGeoCircleFromVector(firstVector, this.geoCircleCache[0]);
                        const planePosVec = state.planePosition.toCartesian(this.vec3Cache[0]);
                        const xtk = Math.abs(firstVectorCircle.distance(planePosVec));
                        if (xtk < UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN)) {
                            const alongVectorNormDistance = FlightPathUtils.getAlongArcNormalizedDistance(firstVectorCircle, this.geoPointCache[0].set(firstVector.startLat, firstVector.startLon), this.geoPointCache[1].set(firstVector.endLat, firstVector.endLon), planePosVec);
                            if (alongVectorNormDistance < 1) {
                                startAtPlanePos = false;
                                retainOldVectors = true;
                            }
                        }
                    }
                }
                if (startAtPlanePos) {
                    state.currentPosition.set(state.planePosition);
                    state.currentCourse = course;
                }
            }
            if (retainOldVectors) {
                // We need to ensure the flight path state is updated to reflect the end of the leg. Note that retainOldVectors
                // can only be true if the leg has at least one calculated vector.
                FlightPathUtils.getLegFinalPosition(leg.calculated, state.currentPosition);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(vectors[vectors.length - 1]);
                state.isDiscontinuity = false;
                state.isFallback = false;
                return;
            }
            if (!state.currentPosition.isValid() || state.isDiscontinuity) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            if (!startAtPlanePos) {
                // If we are not starting the path at the plane position, then calculate an initial turn toward the desired
                // course.
                if (state.currentCourse !== undefined && MathUtils.angularDistanceDeg(state.currentCourse, course, 0) > 1) {
                    const turnDirection = (_b = (_a = FlightPathUtils.getLegDesiredTurnDirection(leg.leg)) !== null && _a !== void 0 ? _a : FlightPathUtils.getShortestTurnDirection(state.currentCourse, course)) !== null && _b !== void 0 ? _b : 'right';
                    vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), turnDirection, state.currentCourse, course, exports.FlightPathVectorFlags.TurnToCourse, heading, isHeadingTrue);
                    if (vectorIndex > 0) {
                        const lastVector = vectors[vectorIndex - 1];
                        state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                        state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
                    }
                }
            }
            const originVec = state.currentPosition.toCartesian(this.vec3Cache[0]);
            const finalPath = this.geoCircleCache[0].setAsGreatCircle(state.currentPosition, course);
            vectorIndex += this.buildDistanceToAltitudeVector(vectors, vectorIndex, leg, isActiveLeg, state, finalPath, originVec, exports.FlightPathVectorFlags.ConstantHeading, heading, isHeadingTrue);
            vectors.length = vectorIndex;
            // NOTE: the vector array cannot be empty because buildDistanceToAltitudeVector() always builds one vector.
            const lastVector = vectors[vectorIndex - 1];
            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Builds vectors representing great-circle paths that intercept other geo circles.
     */
    class CircleInterceptVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        build(vectors, index, start, pathArg, circleToIntercept, flags = 0, heading = null, isHeadingTrue = false) {
            if (circleToIntercept.includes(start)) {
                return 0;
            }
            let path;
            if (pathArg instanceof GeoCircle) {
                if (!pathArg.includes(start)) {
                    throw new Error('CircleInterceptVectorBuilder::build(): the starting point does not lie on the starting path.');
                }
                path = pathArg;
            }
            else {
                path = CircleInterceptVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, pathArg);
            }
            const intersections = CircleInterceptVectorBuilder.intersectionCache;
            const numIntersections = path.intersection(circleToIntercept, intersections);
            if (numIntersections === 0) {
                return 0;
            }
            const intersectionIndex = (numIntersections === 1 || circleToIntercept.encircles(start)) ? 0 : 1;
            const endVec = intersections[intersectionIndex];
            return this.circleVectorBuilder.build(vectors, index, path, start, endVec, flags, heading, isHeadingTrue);
        }
    }
    CircleInterceptVectorBuilder.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
    CircleInterceptVectorBuilder.intersectionCache = [new Float64Array(3), new Float64Array(3)];

    /**
     * Builds vectors representing paths directly connecting a defined initial point and course and a defined end point.
     */
    class DirectToPointVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        build(vectors, index, start, startPath, end, desiredTurnRadius, desiredTurnDirection, flags = 0, includeTurnToCourseFlag = true, includeDirectFlag = true, heading = null, isHeadingTrue = false) {
            let vectorIndex = index;
            if (typeof startPath === 'number') {
                startPath = DirectToPointVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, startPath);
            }
            const endPos = DirectToPointVectorBuilder.geoPointCache[0];
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, DirectToPointVectorBuilder.vec3Cache[0]);
            }
            if (!(end instanceof Float64Array)) {
                endPos.set(end);
                end = GeoPoint.sphericalToCartesian(end, DirectToPointVectorBuilder.vec3Cache[1]);
            }
            else {
                endPos.setFromCartesian(end);
            }
            const distanceToEnd = Vec3Math.unitAngle(start, end);
            if (distanceToEnd <= GeoMath.ANGULAR_TOLERANCE) {
                // The end point is coincident with the starting point. In this case we don't build any vectors.
                return vectorIndex - index;
            }
            else if (Math.abs(distanceToEnd - Math.PI) <= GeoMath.ANGULAR_TOLERANCE) {
                // The end point is antipodal to the starting point. In this case we will build a single great-circle vector
                // along the starting path from the starting point to the end point. All great-circle paths connecting antipodal
                // points are the same length, so staying on the starting path still gets us to the end point in the shortest
                // distance.
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startPath, start, end, flags, heading, isHeadingTrue);
                return vectorIndex - index;
            }
            const startPathEncirclesEnd = startPath.encircles(end);
            const startPathIncludesEnd = startPath.includes(end);
            const turnDirection = desiredTurnDirection !== null && desiredTurnDirection !== void 0 ? desiredTurnDirection : (startPathEncirclesEnd && !startPathIncludesEnd ? 'left' : 'right');
            const startToTurnCenterPath = DirectToPointVectorBuilder.geoCircleCache[1].set(turnDirection === 'left'
                ? Vec3Math.cross(start, startPath.center, DirectToPointVectorBuilder.vec3Cache[2])
                : Vec3Math.cross(startPath.center, start, DirectToPointVectorBuilder.vec3Cache[2]), MathUtils.HALF_PI);
            let maxTurnRadiusRad;
            if (!startPathIncludesEnd && startPathEncirclesEnd === (turnDirection === 'left')) {
                // The end point lies on the same side of the start path as the turn, which means there is the possibility that
                // the turn circle can encircle the end point. This would make it impossible to define a great circle that
                // intersects the end point and is also tangent to the turn circle. Therefore, we compute a maximum allowed turn
                // radius, defined as the radius such that the end point lies exactly on the turn circle.
                const startToTerminatorPathNormal = GeoCircle.getGreatCircleNormal(start, end, DirectToPointVectorBuilder.vec3Cache[2]);
                // The angle between the great-circle path from the start point to the turn center and the great-circle path from
                // the start point to the end point.
                const theta = Vec3Math.unitAngle(startToTurnCenterPath.center, startToTerminatorPathNormal);
                maxTurnRadiusRad = Math.atan(Math.sin(distanceToEnd) / (Math.cos(theta) * (1 + Math.cos(distanceToEnd))));
            }
            else {
                // The end point lies on the starting path or on the opposite side of the starting path as the turn. Either way,
                // no turn can encircle the end point. Therefore there is no maximum turn radius.
                maxTurnRadiusRad = Math.PI / 2;
            }
            const turnRadiusRad = Math.min(maxTurnRadiusRad, UnitType.METER.convertTo(desiredTurnRadius, UnitType.GA_RADIAN));
            const turnCenterVec = startToTurnCenterPath.offsetDistanceAlong(start, turnRadiusRad, DirectToPointVectorBuilder.vec3Cache[2]);
            // Find the great-circle path from the terminator fix that is tangent to the turn circle. There are guaranteed to
            // be two such paths. We choose between the two based on the initial turn direction.
            const turnCenterToTerminatorDistance = Vec3Math.unitAngle(turnCenterVec, end);
            // The angle between the great-circle path from the end point to the turn center and each of the great-circle paths
            // tthat include the end point and are tangent to the turn circle.
            const alpha = Math.asin(Math.min(1, Math.sin(turnRadiusRad) / Math.sin(turnCenterToTerminatorDistance)));
            const finalPath = DirectToPointVectorBuilder.geoCircleCache[1].setAsGreatCircle(turnCenterVec, end).rotate(end, alpha * (turnDirection === 'left' ? -1 : 1), Math.PI);
            const turnEndVec = finalPath.closest(turnCenterVec, DirectToPointVectorBuilder.vec3Cache[3]);
            flags |= includeDirectFlag ? exports.FlightPathVectorFlags.Direct : 0;
            if (Vec3Math.unitAngle(turnEndVec, start) > GeoMath.ANGULAR_TOLERANCE) {
                vectorIndex += this.circleVectorBuilder.buildTurn(vectors, vectorIndex, UnitType.GA_RADIAN.convertTo(turnRadiusRad, UnitType.METER), turnDirection, turnCenterVec, start, turnEndVec, flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : 0), heading, isHeadingTrue);
            }
            if (Vec3Math.unitAngle(turnEndVec, end) > GeoMath.ANGULAR_TOLERANCE) {
                vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, turnEndVec, end, undefined, flags, heading, isHeadingTrue);
            }
            return vectorIndex - index;
        }
    }
    DirectToPointVectorBuilder.vec3Cache = ArrayUtils.create(4, () => Vec3Math.create());
    DirectToPointVectorBuilder.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
    DirectToPointVectorBuilder.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * Calculates flight path vectors for legs with great-circle paths that terminate when intercepting another geo circle.
     */
    class CircleInterceptLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of CircleInterceptLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         * @param isHeadingLeg Whether this calculator calculates flight plan legs flown with constant heading.
         */
        constructor(facilityCache, isHeadingLeg) {
            super(facilityCache, !isHeadingLeg);
            this.isHeadingLeg = isHeadingLeg;
            this.__vec3Cache = ArrayUtils.create(10, () => Vec3Math.create());
            this.__geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.__geoCircleCache = ArrayUtils.create(7, () => new GeoCircle(Vec3Math.create(), 0));
            this.__intersectionCache = ArrayUtils.create(1, () => [Vec3Math.create(), Vec3Math.create()]);
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.circleInterceptVectorBuilder = new CircleInterceptVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
            this.interceptGreatCircleToPointVectorBuilder = new InterceptGreatCircleToPointVectorBuilder();
            this.interceptCourseInfo = {
                course: 0,
                heading: null,
                isHeadingTrue: false
            };
            this.pathToInterceptInfo = {
                circle: new GeoCircle(Vec3Math.create(), 0),
                start: Vec3Math.create(NaN, NaN, NaN),
                end: Vec3Math.create(NaN, NaN, NaN)
            };
            this.desiredIntersectionInfo = {
                isStartPastPathToIntercept: false,
                desiredIntersection: Vec3Math.create(NaN, NaN, NaN)
            };
            this.fallbackInterceptInfo = {
                isStartPastPathToIntercept: false,
                fallbackInterceptPoint: Vec3Math.create(NaN, NaN, NaN)
            };
            this.desiredIntersectionInfoCache = {
                intersection: ArrayUtils.create(1, () => [Vec3Math.create(), Vec3Math.create()]),
            };
            this.fallbackInterceptCache = {
                vec3: ArrayUtils.create(1, () => Vec3Math.create())
            };
            this.fallbackDesiredIntersectionInfo = {
                isStartPastPathToIntercept: false,
                desiredIntersection: Vec3Math.create(NaN, NaN, NaN)
            };
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            var _a, _b, _c, _d;
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            if (state.isDiscontinuity || !state.currentPosition.isValid()) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const interceptCourseInfo = this.getInterceptCourseInfo(legs, calculateIndex, activeLegIndex, state, this.interceptCourseInfo);
            const pathToInterceptInfo = this.getPathToInterceptInfo(legs, calculateIndex, activeLegIndex, state, this.pathToInterceptInfo);
            if (!interceptCourseInfo || !pathToInterceptInfo) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const isActiveLeg = calculateIndex === activeLegIndex;
            let startAtPlanePos = false;
            let retainOldVectors = false;
            if (isActiveLeg && interceptCourseInfo.heading !== null && state.planePosition.isValid()) {
                // If the leg to calculate is the active leg and a fly-heading leg and we know the airplane's current position,
                // then we should start the leg path at the airplane's current position. The exception to this is if non-fallback
                // vectors have been previously calculated and include an initial turn and the airplane's current position is
                // within a certain cross-track distance of the turn and before the end of the turn. In this case, we will
                // retain the previously calculated vectors.
                startAtPlanePos = true;
                if (vectors.length > 0) {
                    const firstVector = vectors[0];
                    if (!BitFlags.isAll(firstVector.flags, exports.FlightPathVectorFlags.Fallback) && !FlightPathUtils.isVectorGreatCircle(firstVector)) {
                        const firstVectorCircle = FlightPathUtils.setGeoCircleFromVector(firstVector, this.__geoCircleCache[0]);
                        const planePosVec = state.planePosition.toCartesian(this.__vec3Cache[0]);
                        const xtk = Math.abs(firstVectorCircle.distance(planePosVec));
                        if (xtk < UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN)) {
                            const alongVectorNormDistance = FlightPathUtils.getAlongArcNormalizedDistance(firstVectorCircle, this.__geoPointCache[0].set(firstVector.startLat, firstVector.startLon), this.__geoPointCache[1].set(firstVector.endLat, firstVector.endLon), planePosVec);
                            if (alongVectorNormDistance < 1) {
                                startAtPlanePos = false;
                                retainOldVectors = true;
                            }
                        }
                    }
                }
                if (startAtPlanePos) {
                    state.currentPosition.set(state.planePosition);
                    state.currentCourse = interceptCourseInfo.course;
                }
            }
            if (retainOldVectors) {
                // We need to ensure the flight path state is updated to reflect the end of the leg. Note that retainOldVectors
                // can only be true if the leg has at least one calculated vector.
                FlightPathUtils.getLegFinalPosition(leg.calculated, state.currentPosition);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(vectors[vectors.length - 1]);
                state.isDiscontinuity = false;
                state.isFallback = leg.calculated.endsInFallback;
                return;
            }
            const pathToInterceptHasStart = Vec3Math.isFinite(pathToInterceptInfo.start);
            const pathTointerceptHasEnd = Vec3Math.isFinite(pathToInterceptInfo.end);
            const effectivePathToInterceptStartVec = pathToInterceptHasStart
                ? pathToInterceptInfo.start
                : pathTointerceptHasEnd
                    ? pathToInterceptInfo.circle.offsetAngleAlong(pathToInterceptInfo.end, -Math.PI, this.__vec3Cache[0], Math.PI)
                    : undefined;
            const effectivePathToInterceptEndVec = pathTointerceptHasEnd
                ? pathToInterceptInfo.end
                : pathToInterceptHasStart
                    ? pathToInterceptInfo.circle.offsetAngleAlong(pathToInterceptInfo.start, Math.PI, this.__vec3Cache[1], Math.PI)
                    : undefined;
            const effectivePathToInterceptAngularWidth = pathToInterceptHasStart && pathTointerceptHasEnd
                ? pathToInterceptInfo.circle.angleAlong(pathToInterceptInfo.start, pathToInterceptInfo.end, Math.PI)
                : effectivePathToInterceptStartVec
                    ? Math.PI
                    : MathUtils.TWO_PI;
            const initialVec = state.currentPosition.toCartesian(this.__vec3Cache[2]);
            const initialCourse = (_a = state.currentCourse) !== null && _a !== void 0 ? _a : interceptCourseInfo.course;
            const initialPos = this.__geoPointCache[0].set(state.currentPosition);
            let startPath = this.__geoCircleCache[0].setAsGreatCircle(state.currentPosition, initialCourse);
            let interceptPathStartVec = initialVec;
            let fallbackCalcStartPos = initialPos;
            let fallbackCalcStartVec = initialVec;
            let fallbackCalcStartCourse = initialCourse;
            let needHandleDefaultCase = true;
            let needBuildInterceptVectors = true;
            let forceFallback = false;
            let useFallbackVectors = false;
            const fallbackInterceptInfo = this.fallbackInterceptInfo;
            // If the leg starts in a fallback state, we are not starting the leg at the airplane's current position, and there
            // is a leg before the one being calculated, then attempt to get the terminator position of the previous leg. If
            // successful, then calculate where a fallback intercept point would be located if the leg were to start at the
            // previous leg's terminator position and force a fallback path to be built to that point.
            if (state.isFallback && !startAtPlanePos && calculateIndex > 0) {
                const prevLeg = legs[calculateIndex - 1];
                const prevLegTerminatorPos = this.getTerminatorPosition(prevLeg.leg, this.__geoPointCache[1]);
                if (prevLegTerminatorPos) {
                    fallbackCalcStartPos = prevLegTerminatorPos;
                    fallbackCalcStartVec = fallbackCalcStartPos.toCartesian(this.__vec3Cache[3]);
                    fallbackCalcStartCourse = (_b = this.getLegTrueCourse(prevLeg.leg)) !== null && _b !== void 0 ? _b : interceptCourseInfo.course;
                    forceFallback = true;
                    useFallbackVectors = true;
                }
            }
            // ---- SPECIAL CASE A ----
            // The path to intercept is a great circle and the intercept course heads away from the path to intercept.
            if (pathToInterceptInfo.circle.isGreatCircle()) {
                const interceptPath = this.__geoCircleCache[1].setAsGreatCircle(fallbackCalcStartPos, interceptCourseInfo.course);
                const desiredIntersectionInfo = this.calculateDesiredIntersectionInfo(fallbackCalcStartVec, interceptPath, pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, effectivePathToInterceptAngularWidth, this.desiredIntersectionInfo);
                if (desiredIntersectionInfo.isStartPastPathToIntercept) {
                    let needHandleOvershootCase = true;
                    let initialPath;
                    if (interceptCourseInfo.course !== fallbackCalcStartCourse) {
                        initialPath = this.__geoCircleCache[2].setAsGreatCircle(fallbackCalcStartPos, fallbackCalcStartCourse);
                        this.calculateFallbackInterceptInfo(fallbackCalcStartVec, initialPath, pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, effectivePathToInterceptAngularWidth, forceFallback, fallbackInterceptInfo);
                        needHandleOvershootCase = true;
                        if (!fallbackInterceptInfo.isStartPastPathToIntercept) {
                            // ---- SPECIAL CASE A.1 ----
                            // The initial course is directed toward the path to intercept. In this case we will ignore the intercept
                            // course and attempt to intercept the path to intercept along the initial course.
                            useFallbackVectors = true;
                            needHandleDefaultCase = true;
                            needHandleOvershootCase = false;
                        }
                        else if (!forceFallback && !startAtPlanePos && MathUtils.angularDistanceDeg(initialCourse, interceptCourseInfo.course, 0) > 1) {
                            // ---- SPECIAL CASE A.2 ----
                            // The initial course is directed away from the path to intercept, we are not starting at the airplane's
                            // current position, and there is an initial turn toward the intercept course. In this case we need to
                            // check whether the initial turn toward the intercept course also turns toward the path to intercept.
                            // Because both the initial and intercept courses are directed away from the path to intercept, the initial
                            // turn will turn toward the path to intercept if and only if it passes through 180 degrees of arc or more.
                            // This is equivalent to the turn direction not being the direction that results in the shortest turn from
                            // the initial course to the intercept course.
                            const turnDirection = FlightPathUtils.getLegDesiredTurnDirection(leg.leg);
                            // Note that if the initial course and intercept course are antiparallel (180 degrees apart), then
                            // FlightPathUtils_G::GetShortestTurnDirection() returns an undefined value, which is never equal to
                            // turnDirection if turnDirection is defined.
                            if (turnDirection !== undefined && turnDirection !== FlightPathUtils.getShortestTurnDirection(initialCourse, interceptCourseInfo.course)) {
                                // The initial turn does turn toward the path to intercept. In this case, we can attempt to construct
                                // a path that consists of the initial turn toward the path to intercept connected to a final turn in the
                                // same direction as the initial turn that intercepts the path to intercept.
                                needHandleOvershootCase = false;
                                const turnDirectionSign = turnDirection === 'left' ? -1 : 1;
                                const initialTurnStartRadial = this.__geoCircleCache[3].setAsGreatCircle(initialVec, initialPath.center);
                                const turnRadiusRad = state.desiredTurnRadius.asUnit(UnitType.GA_RADIAN);
                                const initialTurnCenterVec = initialTurnStartRadial.offsetDistanceAlong(initialVec, turnRadiusRad * -turnDirectionSign, this.__vec3Cache[4], Math.PI);
                                const initialTurnCircle = FlightPathUtils.getTurnCircle(initialTurnCenterVec, turnRadiusRad, turnDirection, this.__geoCircleCache[3]);
                                const initialTurnEndVec = initialTurnCircle.offsetAngleAlong(initialVec, MathUtils.angularDistanceDeg(initialCourse, interceptCourseInfo.course, turnDirectionSign) * Avionics.Utils.DEG2RAD, this.__vec3Cache[5], Math.PI);
                                // The desired angle at which to intercept the path to intercept.
                                const interceptAngleRad = Vec3Math.unitAngle(GeoCircle.getGreatCircleNormal(initialTurnEndVec, initialTurnCircle, this.__vec3Cache[6]), pathToInterceptInfo.circle.center);
                                // The signed distance from the path to intercept to the center of the initial turn. Negative values
                                // indicate that the turn center is encircled by the path to intercept's geo circle.
                                const initialTurnCenterDistance = pathToInterceptInfo.circle.distance(initialTurnCenterVec);
                                // The center of the initial turn projected onto the path to intercept.
                                const initialTurnCenterProjected = pathToInterceptInfo.circle.closest(initialTurnCenterVec, this.__vec3Cache[6]);
                                // Only proceed if the projection was successful. If it was not successful, then that means we are
                                // either very far away from the path to intercept or the turn radius is unreasonably large. Either way,
                                // we will bail out and fall through to the default case.
                                if (Vec3Math.isFinite(initialTurnCenterProjected)) {
                                    // Calculate the position of a final turn that intercepts the path to intercept at exactly the
                                    // desired intercept angle. The center of this final turn lies along the great circle passing
                                    // through the center of the initial turn and initialTurnCenterProjected.
                                    // The signed distance from the path to intercept to the center of the final turn. Negative values
                                    // indicate that the turn center is encircled by the path to intercept's geo circle.
                                    const finalTurnCenterDistance = Math.asin(Math.sin(interceptAngleRad) * Math.sin(turnRadiusRad))
                                        * (interceptAngleRad < MathUtils.HALF_PI ? turnDirectionSign : -turnDirectionSign);
                                    // The distance from the final turn center projected onto the path to intercept (which is equal to
                                    // initialTurnCenterProjected) to the intercept point along the path to intercept.
                                    const interceptOffset = Math.abs(Math.atan(Math.cos(interceptAngleRad) * Math.tan(turnRadiusRad)))
                                        * (pathToInterceptInfo.circle.encircles(initialVec) ? -turnDirectionSign : turnDirectionSign);
                                    const signedTurnCenterDelta = -turnDirectionSign * (finalTurnCenterDistance - initialTurnCenterDistance);
                                    if (signedTurnCenterDelta > GeoMath.ANGULAR_TOLERANCE) {
                                        // The initial and final turns are positioned such that a great-circle path is needed to connect the
                                        // two. Therefore, we will calculate the connecting path.
                                        const finalTurnCenterVec = this.__geoCircleCache[4].setAsGreatCircle(pathToInterceptInfo.circle.center, initialTurnCenterProjected)
                                            .offsetDistanceAlong(initialTurnCenterProjected, finalTurnCenterDistance, this.__vec3Cache[7], Math.PI);
                                        const finalTurnCircle = FlightPathUtils.getTurnCircle(finalTurnCenterVec, turnRadiusRad, turnDirection, this.__geoCircleCache[4]);
                                        // The set of centers of great circles that are tangent to the initial turn.
                                        const initialTurnTangentCenters = this.__geoCircleCache[5].set(initialTurnCircle.center, Math.abs(MathUtils.HALF_PI - initialTurnCircle.radius));
                                        // The set of centers of great circles that are tangent to the final turn.
                                        const finalTurnTangentCenters = this.__geoCircleCache[6].set(finalTurnCircle.center, Math.abs(MathUtils.HALF_PI - finalTurnCircle.radius));
                                        const connectingPathCenterCandidates = this.__intersectionCache[0];
                                        const connectingPathCenterCount = initialTurnTangentCenters.intersection(finalTurnTangentCenters, connectingPathCenterCandidates);
                                        // There should always be two great circles tangent to two non-concentric small circles. If we can't
                                        // find them both, then we will bail out and fall through to the default case.
                                        if (connectingPathCenterCount === 2) {
                                            const connectingPath = this.__geoCircleCache[5].set(connectingPathCenterCandidates[0], MathUtils.HALF_PI);
                                            connectingPath.closest(initialTurnCenterVec, initialTurnEndVec);
                                            const finalTurnStartVec = connectingPath.closest(finalTurnCenterVec, this.__vec3Cache[8]);
                                            const interceptVec = pathToInterceptInfo.circle.offsetDistanceAlong(initialTurnCenterProjected, interceptOffset, this.__vec3Cache[9], Math.PI);
                                            // Build vectors for the initial turn, the connecting path, and the final turn.
                                            if (Vec3Math.unitAngle(initialVec, initialTurnEndVec) > GeoMath.ANGULAR_TOLERANCE) {
                                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, initialTurnCircle, initialVec, initialTurnEndVec, exports.FlightPathVectorFlags.TurnToCourse | exports.FlightPathVectorFlags.Fallback, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                                            }
                                            if (Vec3Math.unitAngle(initialTurnEndVec, finalTurnStartVec) > GeoMath.ANGULAR_TOLERANCE) {
                                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, connectingPath, initialTurnEndVec, finalTurnStartVec, exports.FlightPathVectorFlags.Fallback, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                                            }
                                            if (Vec3Math.unitAngle(finalTurnStartVec, interceptVec) > GeoMath.ANGULAR_TOLERANCE) {
                                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, finalTurnCircle, finalTurnStartVec, interceptVec, exports.FlightPathVectorFlags.TurnToCourse | exports.FlightPathVectorFlags.Fallback, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                                            }
                                            needHandleDefaultCase = false;
                                            needBuildInterceptVectors = false;
                                        }
                                    }
                                    else if (signedTurnCenterDelta < -GeoMath.ANGULAR_TOLERANCE) ;
                                    else {
                                        // The initial and final turns are coincident. This means that we can intercept the path to intercept
                                        // by staying on the initial turn.
                                        const interceptVec = pathToInterceptInfo.circle.offsetDistanceAlong(initialTurnCenterProjected, interceptOffset, this.__vec3Cache[7], Math.PI);
                                        if (effectivePathToInterceptAngularWidth === MathUtils.TWO_PI
                                            || FlightPathUtils.isPointAlongArc(pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptAngularWidth, interceptVec, true)) {
                                            // The intercept point is within the bounds of the path to intercept. We will build the initial
                                            // turn and end it at the intercept point.
                                            vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, initialTurnCircle, initialVec, interceptVec, exports.FlightPathVectorFlags.TurnToCourse | exports.FlightPathVectorFlags.Fallback, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                                            needHandleDefaultCase = false;
                                            needBuildInterceptVectors = false;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    else {
                        initialPath = interceptPath;
                    }
                    if (needHandleOvershootCase) {
                        // ---- SPECIAL CASE A.3 ----
                        // Both the intercept course and the initial course are directed away from the path to intercept (assuming)
                        // both start at the beginning of the leg) and the initial turn toward the intercept course either doesn't
                        // exist or does not turn toward the path to intercept. In this case the beginning of the leg has effectively
                        // "overshot" the path to intercept. Therefore, we will construct a fallback path that turns back toward the
                        // path to intercept on a 45-degree intercept path.
                        // First we need to figure out in which direction to intercept the path to intercept. There are two such
                        // directions that produce a 45-degree intercept angle. We will choose the direction that is the closest to
                        // the direction we would have intercepted the path to intercept along the desired intercept course had we
                        // not "overshot" it.
                        const interceptAngle = Vec3Math.dot(interceptPath.center, pathToInterceptInfo.circle.center) >= 0 ? 45 : 135;
                        const flags = exports.FlightPathVectorFlags.Fallback;
                        const turnFlags = exports.FlightPathVectorFlags.TurnToCourse | exports.FlightPathVectorFlags.Fallback;
                        const vectorCount = this.interceptGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, fallbackCalcStartVec, initialPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, interceptAngle, undefined, pathToInterceptInfo.circle, undefined, turnFlags, flags, turnFlags, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                        if (vectorCount > 0) {
                            vectorIndex += vectorCount;
                            if (effectivePathToInterceptAngularWidth !== MathUtils.TWO_PI) {
                                // If the path to intercept has a defined start and end, then check if the calculated intercept lies in
                                // bounds. If not, then revert to a direct-to path from the beginning of the leg to the path to intercept
                                // endpoint (start or end) that is closer to the calculated intercept.
                                const lastVector = vectors[vectorIndex - 1];
                                const interceptVec = GeoPoint.sphericalToCartesian(lastVector.endLat, lastVector.endLon, this.__vec3Cache[4]);
                                const interceptAlongArcNormalizedDistance = FlightPathUtils.getAlongArcNormalizedDistance(pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, interceptVec);
                                let directToTarget;
                                if (interceptAlongArcNormalizedDistance < 0) {
                                    // The intercept point lies out of bounds, but closer to the start of the path to intercept than the end.
                                    directToTarget = effectivePathToInterceptStartVec;
                                }
                                else if (interceptAlongArcNormalizedDistance > 1) {
                                    // The intercept point lies out of bounds, but closer to the end of the path to intercept than the start.
                                    directToTarget = effectivePathToInterceptEndVec;
                                }
                                if (directToTarget) {
                                    vectorIndex -= vectorCount;
                                    vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, fallbackCalcStartVec, initialPath, directToTarget, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Fallback, true, true, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                                }
                            }
                            needHandleDefaultCase = false;
                            needBuildInterceptVectors = false;
                        }
                    }
                }
            }
            // ---- DEFAULT CASE ----
            if (needHandleDefaultCase) {
                if (forceFallback) {
                    // If a fallback path is being forced, then skip any initial turn and calculate the fallback intercept point
                    // with the intercept path starting from the beginning of the leg.
                    const interceptPath = this.__geoCircleCache[1].setAsGreatCircle(interceptPathStartVec, interceptCourseInfo.course);
                    this.calculateFallbackInterceptInfo(initialVec, interceptPath, pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, effectivePathToInterceptAngularWidth, true, fallbackInterceptInfo);
                }
                else {
                    // If a fallback path is not being forced, then calculate an initial turn to the intercept course if necessary.
                    let initialTurnVector;
                    const includeInitialTurn = !startAtPlanePos && MathUtils.angularDistanceDeg(initialCourse, interceptCourseInfo.course, 0) > 1;
                    if (includeInitialTurn) {
                        const turnDirection = (_d = (_c = FlightPathUtils.getLegDesiredTurnDirection(leg.leg)) !== null && _c !== void 0 ? _c : FlightPathUtils.getShortestTurnDirection(initialCourse, interceptCourseInfo.course)) !== null && _d !== void 0 ? _d : 'right';
                        vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), turnDirection, initialCourse, interceptCourseInfo.course, exports.FlightPathVectorFlags.TurnToCourse, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                        if (vectorIndex > 0) {
                            initialTurnVector = vectors[vectorIndex - 1];
                            interceptPathStartVec = GeoPoint.sphericalToCartesian(initialTurnVector.endLat, initialTurnVector.endLon, this.__vec3Cache[4]);
                        }
                    }
                    const interceptPath = this.__geoCircleCache[1].setAsGreatCircle(interceptPathStartVec, interceptCourseInfo.course);
                    startPath = interceptPath;
                    // Check if a fallback path is necessary when starting the intercept path from the end of the initial turn if
                    // one exists or from the beginning of the leg if an initial turn was not calculated.
                    this.calculateFallbackInterceptInfo(interceptPathStartVec, interceptPath, pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, effectivePathToInterceptAngularWidth, false, fallbackInterceptInfo);
                    if (initialTurnVector && fallbackInterceptInfo.isStartPastPathToIntercept) {
                        // An initial turn exists and ends past the path to intercept. We need to check if the initial turn
                        // intersects the path to intercept. If it does, then we can end the turn early at the intersection point and
                        // thus avoid having to build a fallback path.
                        const turnCircle = FlightPathUtils.setGeoCircleFromVector(initialTurnVector, this.__geoCircleCache[2]);
                        const intersections = this.__intersectionCache[0];
                        const intersectionCount = turnCircle.intersection(pathToInterceptInfo.circle, intersections);
                        const startIndex = intersectionCount < 2
                            || (pathToInterceptInfo.circle.radius > MathUtils.HALF_PI) === pathToInterceptInfo.circle.encircles(initialVec)
                            ? 0 : 1;
                        for (let i = 0; i < intersectionCount; i++) {
                            const intersection = intersections[(startIndex + i) % 2];
                            if (
                            // Check whether the intersection is within the bounds of the turn vector...
                            FlightPathUtils.isPointAlongArc(turnCircle, initialVec, interceptPathStartVec, intersection, true)
                                // ... and whether the intersection is within the bounds of the path to intercept.
                                && (!effectivePathToInterceptStartVec
                                    || FlightPathUtils.isPointAlongArc(pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptAngularWidth, intersection, true))) {
                                // End the turn early at the intersection (where the turn intersects the path to intercept).
                                const distance = turnCircle.distanceAlong(initialVec, intersection, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                                if (distance > GeoMath.ANGULAR_TOLERANCE) {
                                    // The intersection is not coincident with the start of the turn. We will modify the turn vector to end
                                    // at the intersection.
                                    state.currentPosition.setFromCartesian(intersection);
                                    initialTurnVector.distance = UnitType.GA_RADIAN.convertTo(distance, UnitType.METER);
                                    initialTurnVector.endLat = state.currentPosition.lat;
                                    initialTurnVector.endLon = state.currentPosition.lon;
                                    state.currentCourse = FlightPathUtils.getVectorFinalCourse(initialTurnVector);
                                }
                                else {
                                    // The intersection is coincident with the start of the turn. In this case we will just discard the
                                    // entire turn vector. This also means the leg will end up with no calculated vectors.
                                    --vectorIndex;
                                }
                                vectors.length = vectorIndex;
                                state.isDiscontinuity = false;
                                state.isFallback = false;
                                return;
                            }
                        }
                        // The initial turn does not intersect the path to intercept -> calculate a fallback intercept without an
                        // initial turn (i.e. change the intercept path to start at the start of the leg instead of the end of the
                        // initial turn).
                        vectorIndex = 0;
                        interceptPathStartVec = initialVec;
                        interceptPath.setAsGreatCircle(initialPos, interceptCourseInfo.course);
                        startPath = this.__geoCircleCache[0];
                        this.calculateFallbackInterceptInfo(interceptPathStartVec, interceptPath, pathToInterceptInfo.circle, effectivePathToInterceptStartVec, effectivePathToInterceptEndVec, effectivePathToInterceptAngularWidth, true, fallbackInterceptInfo);
                    }
                }
            }
            if (needBuildInterceptVectors) {
                if (Vec3Math.isFinite(fallbackInterceptInfo.fallbackInterceptPoint)) {
                    vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, interceptPathStartVec, startPath, fallbackInterceptInfo.fallbackInterceptPoint, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Fallback, true, true, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                }
                else {
                    vectorIndex += this.circleInterceptVectorBuilder.build(vectors, vectorIndex, interceptPathStartVec, interceptCourseInfo.course, pathToInterceptInfo.circle, useFallbackVectors ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.ConstantHeading, interceptCourseInfo.heading, interceptCourseInfo.isHeadingTrue);
                }
            }
            vectors.length = vectorIndex;
            if (vectorIndex > 0) {
                const lastVector = vectors[vectorIndex - 1];
                state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
        /**
         * Gets information describing the course to use to intercept the path to intercept for a flight plan leg to
         * calculate.
         * @param legs An array of legs containing the flight plan leg to calculate.
         * @param calculateIndex The index of the flight plan leg to calculate.
         * @param activeLegIndex The index of the active flight plan leg.
         * @param state The current flight path state.
         * @param out The object to which to write the result.
         * @returns Information describing the course to use to intercept the path to intercept for the specified flight plan
         * leg to calculate, or `undefined` if an intercept course could not be defined.
         */
        getInterceptCourseInfo(legs, calculateIndex, activeLegIndex, state, out) {
            const leg = legs[calculateIndex];
            let course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            if (this.isHeadingLeg) {
                const heading = course;
                if (state.planeWindSpeed.number > 0) {
                    course = NavMath.headingToGroundTrack(heading, state.planeTrueAirspeed.asUnit(UnitType.KNOT), state.planeWindDirection, state.planeWindSpeed.asUnit(UnitType.KNOT));
                    if (isNaN(course)) {
                        course = heading;
                    }
                }
                out.heading = leg.leg.course;
                out.isHeadingTrue = leg.leg.trueDegrees;
            }
            else {
                out.heading = null;
                out.isHeadingTrue = false;
            }
            out.course = course;
            return out;
        }
        /**
         * Calculates information describing the desired intersection between a defined intercept path and a path to
         * intercept.
         * @param start The starting point of the intercept path.
         * @param interceptPath A GeoCircle that defines the intercept path. Must be a great circle.
         * @param pathToInterceptCircle A geo circle that defines the path to intercept.
         * @param pathToInterceptStart The start of the path to intercept along its defining GeoCircle, or `undefined` if the
         * path to intercept encompasses the entire circle.
         * @param pathToInterceptEnd The end of the path to intercept along its defining GeoCircle, or `undefined1 if the
         * path to intercept encompasses the entire circle.
         * @param pathToInterceptAngularWidth The angular width of the path to intercept along its defining GeoCircle, in
         * radians.
         * @param out The object to which to write the results.
         * @returns Information describing the desired intersection between a defined intercept path and a path to intercept.
         */
        calculateDesiredIntersectionInfo(start, interceptPath, pathToInterceptCircle, pathToInterceptStart, pathToInterceptEnd, pathToInterceptAngularWidth, out) {
            // Determine if the starting point is past the path to intercept as measured along the intercept path.
            out.isStartPastPathToIntercept = false;
            Vec3Math.set(NaN, NaN, NaN, out.desiredIntersection);
            const intersections = this.desiredIntersectionInfoCache.intersection[0];
            const intersectionCount = interceptPath.intersection(pathToInterceptCircle, intersections);
            if (intersectionCount === 2) {
                // Define the next intersection as the intersection that is encountered first when travelling along the intercept
                // path from the starting point, and the previous intersection as the intersection that is encountered second. In
                // this way, the next intersection is "ahead" of the starting point and the previous intersection is "behind" the
                // starting point along the intercept path.
                const nextIntersectionIndex = pathToInterceptCircle.encircles(start) ? 0 : 1;
                const nextIntersection = intersections[nextIntersectionIndex];
                const prevIntersection = intersections[(nextIntersectionIndex + 1) % 2];
                // Define the desired intercept point as the one that requires the shortest distance to be travelled in order to
                // move from the starting point to the intercept point along the intercept path and path to intercept.
                if (pathToInterceptAngularWidth === MathUtils.TWO_PI && pathToInterceptCircle.isGreatCircle()) {
                    out.isStartPastPathToIntercept = interceptPath.angleAlong(start, nextIntersection, Math.PI, GeoMath.ANGULAR_TOLERANCE) > MathUtils.HALF_PI + GeoMath.ANGULAR_TOLERANCE;
                }
                else {
                    const prevIntersectionInitialPathOffset = interceptPath.angleAlong(prevIntersection, start, Math.PI);
                    const nextIntersectionInitialPathOffset = interceptPath.angleAlong(start, nextIntersection, Math.PI);
                    const prevIntersectionInitialPathDistance = Math.min(prevIntersectionInitialPathOffset, MathUtils.TWO_PI - prevIntersectionInitialPathOffset);
                    const nextIntersectionInitialPathDistance = Math.min(nextIntersectionInitialPathOffset, MathUtils.TWO_PI - nextIntersectionInitialPathOffset);
                    let prevIntersectionInterceptPathDistance = 0;
                    let nextIntersectionInterceptPathDistance = 0;
                    if (pathToInterceptStart && pathToInterceptEnd) {
                        if (!FlightPathUtils.isPointAlongArc(pathToInterceptCircle, pathToInterceptStart, pathToInterceptAngularWidth, prevIntersection)) {
                            const prevIntersectionInterceptPathStartOffset = pathToInterceptCircle.angleAlong(prevIntersection, pathToInterceptStart, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                            const prevIntersectionInterceptPathEndOffset = pathToInterceptCircle.angleAlong(prevIntersection, pathToInterceptEnd, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                            prevIntersectionInterceptPathDistance = Math.min(prevIntersectionInterceptPathStartOffset, MathUtils.TWO_PI - prevIntersectionInterceptPathStartOffset, prevIntersectionInterceptPathEndOffset, MathUtils.TWO_PI - prevIntersectionInterceptPathEndOffset);
                        }
                        if (!FlightPathUtils.isPointAlongArc(pathToInterceptCircle, pathToInterceptStart, pathToInterceptAngularWidth, nextIntersection)) {
                            const nextIntersectionInterceptPathStartOffset = pathToInterceptCircle.angleAlong(nextIntersection, pathToInterceptStart, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                            const nextIntersectionInterceptPathEndOffset = pathToInterceptCircle.angleAlong(nextIntersection, pathToInterceptEnd, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                            nextIntersectionInterceptPathDistance = Math.min(nextIntersectionInterceptPathStartOffset, MathUtils.TWO_PI - nextIntersectionInterceptPathStartOffset, nextIntersectionInterceptPathEndOffset, MathUtils.TWO_PI - nextIntersectionInterceptPathEndOffset);
                        }
                    }
                    const prevIntersectionTotalDistance = prevIntersectionInitialPathDistance + prevIntersectionInterceptPathDistance;
                    const nextIntersectionTotalDistance = nextIntersectionInitialPathDistance + nextIntersectionInterceptPathDistance;
                    // Only consider the starting position past the path to intercept if the path to intercept has a defined
                    // start and end (i.e. is not a DME circle) OR the distance to one of the two intercept points is greater
                    // than pi / 2 great-arc radians.
                    if ((pathToInterceptStart && pathToInterceptEnd)
                        || prevIntersectionTotalDistance > MathUtils.HALF_PI + GeoMath.ANGULAR_TOLERANCE
                        || nextIntersectionTotalDistance > MathUtils.HALF_PI + GeoMath.ANGULAR_TOLERANCE) {
                        out.isStartPastPathToIntercept = prevIntersectionTotalDistance < nextIntersectionTotalDistance - GeoMath.ANGULAR_TOLERANCE;
                    }
                }
                Vec3Math.copy(out.isStartPastPathToIntercept ? prevIntersection : nextIntersection, out.desiredIntersection);
            }
            else if (intersectionCount === 1) {
                const distanceToIntersection = interceptPath.angleAlong(start, intersections[0], Math.PI, GeoMath.ANGULAR_TOLERANCE);
                out.isStartPastPathToIntercept = distanceToIntersection < MathUtils.TWO_PI - GeoMath.ANGULAR_TOLERANCE
                    && distanceToIntersection > Math.PI + GeoMath.ANGULAR_TOLERANCE;
                Vec3Math.copy(intersections[0], out.desiredIntersection);
            }
            return out;
        }
        /**
         * Calculates information describing a potential fallback path that intercepts a path to intercept from a defined
         * starting point.
         * @param start The starting point of the intercept path.
         * @param interceptPath A GeoCircle that defines the intercept path. Must be a great circle.
         * @param pathToInterceptCircle A geo circle that defines the path to intercept.
         * @param pathToInterceptStart The start of the path to intercept along its defining GeoCircle, or `undefined` if the
         * path to intercept encompasses the entire circle.
         * @param pathToInterceptEnd The end of the path to intercept along its defining GeoCircle, or `undefined1 if the
         * path to intercept encompasses the entire circle.
         * @param pathToInterceptAngularWidth The angular width of the path to intercept along its defining GeoCircle, in
         * radians.
         * @param forceFallback Whether to force a fallback intercept point to be calculated even under conditions where it
         * is not otherwise required.
         * @param out The object to which to write the results.
         * @returns Information describing a potential fallback path that intercepts the specified path to intercept from the
         * defined starting point.
         */
        calculateFallbackInterceptInfo(start, interceptPath, pathToInterceptCircle, pathToInterceptStart, pathToInterceptEnd, pathToInterceptAngularWidth, forceFallback, out) {
            // Check whether the starting point lies on the path to intercept. If so, there is no need for a fallback path
            // unless one is forced, in which case the point that the fallback intercept point is just the starting point.
            if (pathToInterceptCircle.includes(start)) {
                if (pathToInterceptAngularWidth === MathUtils.TWO_PI
                    || FlightPathUtils.isPointAlongArc(pathToInterceptCircle, pathToInterceptStart, pathToInterceptAngularWidth, start, true)) {
                    out.isStartPastPathToIntercept = false;
                    if (forceFallback) {
                        Vec3Math.copy(start, out.fallbackInterceptPoint);
                    }
                    else {
                        Vec3Math.set(NaN, NaN, NaN, out.fallbackInterceptPoint);
                    }
                    return out;
                }
            }
            // Determine if the starting point is past the path to intercept as measured along the intercept path.
            const desiredIntersectionInfo = this.calculateDesiredIntersectionInfo(start, interceptPath, pathToInterceptCircle, pathToInterceptStart, pathToInterceptEnd, pathToInterceptAngularWidth, this.fallbackDesiredIntersectionInfo);
            let desiredInterceptPoint;
            if (!Vec3Math.isFinite(desiredIntersectionInfo.desiredIntersection)) {
                // If the intercept path does not intersect the path to intercept at all, then define the desired intercept
                // point as the point on the path to intercept that is closest to the start point.
                desiredInterceptPoint = pathToInterceptCircle.closest(start, this.fallbackInterceptCache.vec3[0]);
            }
            else {
                // If the starting point is past the path to intercept and the latter is a great circle, then set the desired
                // intercept point to the point on the path to intercept that is closest to the start point. This prevents us
                // from choosing a desired intercept point that is unnecessarily far from the start point if the intercept
                // path intersects the path to intercept at a shallow angle.
                //
                // In all other cases, set the desired intercept point to one of the intersections between the intercept path
                // and the path to intercept.
                if (desiredIntersectionInfo.isStartPastPathToIntercept && pathToInterceptCircle.isGreatCircle()) {
                    desiredInterceptPoint = pathToInterceptCircle.closest(start, this.fallbackInterceptCache.vec3[0]);
                }
                else {
                    desiredInterceptPoint = desiredIntersectionInfo.desiredIntersection;
                }
            }
            out.isStartPastPathToIntercept = desiredIntersectionInfo.isStartPastPathToIntercept;
            if ((pathToInterceptStart === undefined || pathToInterceptEnd === undefined)
                || FlightPathUtils.isPointAlongArc(pathToInterceptCircle, pathToInterceptStart, pathToInterceptAngularWidth, desiredInterceptPoint, true)) {
                // The desired intercept point is within the bounds of the path to intercept. Only handle the fallback if the
                // starting point is past the path to intercept or the intercept path does not intersect the path to intercept
                // or fallback is forced.
                if (forceFallback
                    || desiredIntersectionInfo.isStartPastPathToIntercept
                    || !Vec3Math.isFinite(desiredIntersectionInfo.desiredIntersection)) {
                    Vec3Math.copy(desiredInterceptPoint, out.fallbackInterceptPoint);
                }
                else {
                    Vec3Math.set(NaN, NaN, NaN, out.fallbackInterceptPoint);
                }
            }
            else {
                // The desired intercept point is not within the bounds of the path to intercept. Set the fallback intercept
                // point to the start or end of the path to intercept, whichever is closer to the desired intercept point.
                const angularOffset = pathToInterceptCircle.angleAlong(pathToInterceptStart, desiredInterceptPoint, Math.PI);
                const distanceFromStart = Math.min(angularOffset, MathUtils.TWO_PI - angularOffset);
                const distanceFromEnd = MathUtils.angularDistance(pathToInterceptAngularWidth, angularOffset, 0);
                Vec3Math.copy(distanceFromStart <= distanceFromEnd ? pathToInterceptStart : pathToInterceptEnd, out.fallbackInterceptPoint);
            }
            return out;
        }
    }

    /**
     * Builds vectors representing paths that connect two geo circles.
     */
    class ConnectCirclesVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        /**
         * Builds a sequence of vectors representing a path which consists of a single geo circle which connects two other
         * circles and optionally paths to link the connecting circle with a start point on the from circle and an end point
         * on the to circle.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param fromCircle The circle from which to add the connecting circle.
         * @param toCircle The circle to which to add the connecting circle.
         * @param radius The radius, in meters, of the circle to join the two circles. If not defined, defaults to `pi / 2`
         * times the radius of the Earth (and therefore the connecting circle will be a great circle).
         * @param from The starting point along `fromCircle`. If not defined, this will be assumed to be equal to the
         * point where the connecting circle meets `fromCircle`.
         * @param to The ending point along `toCircle`. If not defined, this will be assumed to be equal to the point where
         * the connecting circle meets `toCircle`.
         * @param fromCircleVectorFlags The flags to set on the vector along `fromCircle`. Defaults to none (0).
         * @param toCircleVectorFlags The flags to set on the vector along the `toCircle`. Defaults to none (0).
         * @param connectVectorFlags The flags to set on the vector connecting `fromCircle` to `toCircle`. Defaults to none
         * (0).
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         */
        build(vectors, index, fromCircle, toCircle, radius, from, to, fromCircleVectorFlags = 0, toCircleVectorFlags = 0, connectVectorFlags = 0, heading = null, isHeadingTrue = false) {
            if (radius !== undefined && radius <= GeoMath.ANGULAR_TOLERANCE) {
                return 0;
            }
            const angle = Vec3Math.unitAngle(fromCircle.center, toCircle.center);
            if ((angle <= GeoMath.ANGULAR_TOLERANCE && fromCircle.radius === toCircle.radius)
                || (Math.PI - angle <= GeoMath.ANGULAR_TOLERANCE && Math.PI - fromCircle.radius - toCircle.radius <= GeoMath.ANGULAR_TOLERANCE)) {
                return 0;
            }
            if (from && !(from instanceof Float64Array)) {
                from = GeoPoint.sphericalToCartesian(from, ConnectCirclesVectorBuilder.vec3Cache[0]);
            }
            if (to && !(to instanceof Float64Array)) {
                to = GeoPoint.sphericalToCartesian(to, ConnectCirclesVectorBuilder.vec3Cache[1]);
            }
            const radiusRad = Math.min(Math.PI / 2, radius ? UnitType.METER.convertTo(radius, UnitType.GA_RADIAN) : Infinity);
            const joinCircle = this.findCircleToJoinCircles(fromCircle, toCircle, radiusRad, ConnectCirclesVectorBuilder.geoCircleCache[0], from, to);
            if (!joinCircle) {
                return 0;
            }
            let vectorIndex = index;
            const joinStart = joinCircle.closest(FlightPathUtils.getTurnCenterFromCircle(fromCircle, ConnectCirclesVectorBuilder.vec3Cache[2]), ConnectCirclesVectorBuilder.vec3Cache[2]);
            const joinEnd = joinCircle.closest(FlightPathUtils.getTurnCenterFromCircle(toCircle, ConnectCirclesVectorBuilder.vec3Cache[3]), ConnectCirclesVectorBuilder.vec3Cache[3]);
            if (from && Vec3Math.unitAngle(from, joinStart) > GeoPoint.EQUALITY_TOLERANCE) {
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, fromCircle, from, joinStart, fromCircleVectorFlags, heading, isHeadingTrue);
            }
            vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, joinCircle, joinStart, joinEnd, connectVectorFlags, heading, isHeadingTrue);
            if (to && Vec3Math.unitAngle(to, joinEnd) > GeoPoint.EQUALITY_TOLERANCE) {
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, toCircle, joinEnd, to, toCircleVectorFlags, heading, isHeadingTrue);
            }
            return vectorIndex - index;
        }
        /**
         * Finds a GeoCircle which connects (is tangent to) two other circles.
         * @param fromCircle The circle at the beginning of the connecting circle.
         * @param toCircle The circle at the end of the connecting circle.
         * @param radius The desired radius of the connecting circle, in great-arc radians.
         * @param out A GeoCircle object to which to write the result.
         * @param from The starting point along `fromCircle`. If not defined, this will be assumed to be equal to the
         * point where the connecting circle meets `fromCircle`.
         * @param to The ending point along `toCircle`. If not defined, this will be assumed to be equal to the point where
         * the connecting circle meets `toCircle`.
         * @returns a GeoCircle which connects the two circles, or null if one could not be found.
         */
        findCircleToJoinCircles(fromCircle, toCircle, radius, out, from, to) {
            /*
             * Theory: the locus of all centers of circle of radius r tangent to circle with center C and radius R is
             * equivalent to the set of circles S(C) with center C and positive radius |r +/- R|. If we further restrict the
             * set of tangent circles to those where both the original and tangent circle run in the same direction at the
             * tangent point, the locus of centers can be further reduced to the single circle Sd(C) with center C and
             * positive radius |r - R|. Therefore, to find the centers of the circles of radius r connecting the circles C1 and
             * C2, we need only find the intersections of Sd(C1) and Sd(C2).
             */
            const solutions = [];
            const intersections = ConnectCirclesVectorBuilder.intersectionCache;
            const leftTurnRadius = radius;
            let fromCircleOffsetRadius = Math.abs(leftTurnRadius - fromCircle.radius);
            let toCircleOffsetRadius = Math.abs(leftTurnRadius - toCircle.radius);
            let fromCircleOffset = ConnectCirclesVectorBuilder.geoCircleCache[1].set(fromCircle.center, fromCircleOffsetRadius);
            let toCircleOffset = ConnectCirclesVectorBuilder.geoCircleCache[2].set(toCircle.center, toCircleOffsetRadius);
            const numLeftTurnSolutions = fromCircleOffset.intersection(toCircleOffset, intersections);
            if (numLeftTurnSolutions === 1) {
                solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[1].set(intersections[0], leftTurnRadius));
            }
            else if (numLeftTurnSolutions === 2) {
                solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[1].set(intersections[0], leftTurnRadius));
                solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[2].set(intersections[1], leftTurnRadius));
            }
            if (radius !== Math.PI / 2) {
                const rightTurnRadius = Math.PI - radius;
                fromCircleOffsetRadius = Math.abs(rightTurnRadius - fromCircle.radius);
                toCircleOffsetRadius = Math.abs(rightTurnRadius - toCircle.radius);
                fromCircleOffset = ConnectCirclesVectorBuilder.geoCircleCache[3].set(fromCircle.center, fromCircleOffsetRadius);
                toCircleOffset = ConnectCirclesVectorBuilder.geoCircleCache[4].set(toCircle.center, toCircleOffsetRadius);
                const numRightTurnSolutions = fromCircleOffset.intersection(toCircleOffset, intersections);
                if (numRightTurnSolutions === 1) {
                    solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[3].set(intersections[0], rightTurnRadius));
                }
                else if (numRightTurnSolutions === 2) {
                    solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[3].set(intersections[0], rightTurnRadius));
                    solutions.push(ConnectCirclesVectorBuilder.geoCircleCache[4].set(intersections[1], rightTurnRadius));
                }
            }
            if (solutions.length === 0) {
                return null;
            }
            else if (solutions.length === 1) {
                return out.set(solutions[0].center, solutions[0].radius);
            }
            else {
                // choose the solution that results in the shortest path from fromVec to toVec
                let circle = solutions[0];
                let minDistance = this.calculateJoinCirclesPathDistance(fromCircle, toCircle, solutions[0], from, to);
                for (let i = 1; i < solutions.length; i++) {
                    const distance = this.calculateJoinCirclesPathDistance(fromCircle, toCircle, solutions[i], from, to);
                    if (distance < minDistance) {
                        circle = solutions[i];
                        minDistance = distance;
                    }
                }
                return out.set(circle.center, circle.radius);
            }
        }
        /**
         * Calculates the total distance along the joining path between two circles.
         * @param fromCircle The circle at the beginning of the connecting circle.
         * @param toCircle The circle at the end of the connecting circle.
         * @param joinCircle The connecting circle.
         * @param from The starting point along `fromCircle`. If not defined, this will be assumed to be equal to the
         * point where the connecting circle meets `fromCircle`.
         * @param to The ending point along `toCircle`. If not defined, this will be assumed to be equal to the point where
         * the connecting circle meets `toCircle`.
         * @returns the total distance along the joining path, in great-arc radians.
         */
        calculateJoinCirclesPathDistance(fromCircle, toCircle, joinCircle, from, to) {
            let distance = 0;
            const joinStartVec = joinCircle.closest(FlightPathUtils.getTurnCenterFromCircle(fromCircle, ConnectCirclesVectorBuilder.vec3Cache[6]), ConnectCirclesVectorBuilder.vec3Cache[6]);
            const joinEndVec = joinCircle.closest(FlightPathUtils.getTurnCenterFromCircle(toCircle, ConnectCirclesVectorBuilder.vec3Cache[7]), ConnectCirclesVectorBuilder.vec3Cache[7]);
            if (from) {
                distance += fromCircle.distanceAlong(from, joinStartVec, Math.PI);
            }
            distance += joinCircle.distanceAlong(joinStartVec, joinEndVec, Math.PI);
            if (to) {
                distance += toCircle.distanceAlong(joinEndVec, to, Math.PI);
            }
            return distance;
        }
    }
    ConnectCirclesVectorBuilder.vec3Cache = ArrayUtils.create(8, () => Vec3Math.create());
    ConnectCirclesVectorBuilder.geoCircleCache = ArrayUtils.create(5, () => new GeoCircle(Vec3Math.create(), 0));
    ConnectCirclesVectorBuilder.intersectionCache = [new Float64Array(3), new Float64Array(3)];

    /**
     * Builds vectors representing paths connecting a start point and optional initial course to an end point and final
     * course.
     */
    class DirectToJoinGreatCircleAtPointVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.connectCirclesVectorBuilder = new ConnectCirclesVectorBuilder();
        }
        /**
         * Builds a sequence of vectors representing a path from a defined start point and optional initial course which
         * connects with a defined end point and final course.
         *
         * This method attempts to build a path that consists of an initial turn from the start point toward the end point,
         * followed by a great-circle path toward the end point, and ending with a final turn that joins the end point and
         * final course. One or more of these path components may be omitted if they are deemed unnecessary. A procedure turn
         * may be calculated in lieu of the path described above if the procedure turn is determined to be a more optimal
         * path.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param start The start point.
         * @param startPath A GeoCircle that defines the initial course. Must be a great circle. If not defined, then an
         * initial course will be chosen to optimize the path that is built.
         * @param end The end point.
         * @param endPath A GeoCircle that defines the final course. Must be a great circle.
         * @param turnRadius The desired turn radius, in meters.
         * @param startTurnDirection The direction of the initial turn. If not defined, then a direction will be chosen to
         * optimize the path that is built.
         * @param flags The flags to set on all built vectors. Defaults to none (0).
         * @param includeTurnToCourseFlag Whether to include the `TurnToCourse` flag on the vectors that comprise the initial
         * and final turns. Defaults to `true`.
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         * @throws Error if either the start or end path is not a great circle.
         */
        build(vectors, index, start, startPath, end, endPath, turnRadius, startTurnDirection, flags = 0, includeTurnToCourseFlag = true, heading = null, isHeadingTrue = false) {
            if ((startPath && !startPath.isGreatCircle()) || !endPath.isGreatCircle()) {
                throw new Error('DirectToJoinGreatCircleAtPointVectorBuilder::build(): start or end path is not a great circle');
            }
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache[0]);
            }
            if (!(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache[1]);
            }
            const startDistanceFromEndPath = endPath.distance(start);
            const turnRadiusRad = UnitType.METER.convertTo(turnRadius, UnitType.GA_RADIAN);
            let endTurnDirection = startDistanceFromEndPath > 0 ? 'right' : 'left';
            const endTurnCircle = FlightPathUtils.getTurnCircleStartingFromPath(end, endPath, turnRadiusRad, endTurnDirection, DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[1]);
            let startPathEndPathAngleDiff;
            let startToEndOffset;
            if (!startPath && Math.abs(startDistanceFromEndPath) <= GeoMath.ANGULAR_TOLERANCE) {
                // We can choose the start path and the start point lies on the end path. We will choose a direction for the
                // start path that is the shortest path from the start point to the end point.
                startPath = DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[0];
                const startToEndDistance = endPath.angleAlong(start, end, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                if (startToEndDistance === 0) {
                    // The start and end points are coincident. There is no path to build in this case, so we will return
                    // immediately.
                    return 0;
                }
                else if (startToEndDistance <= Math.PI) {
                    startPath.set(endPath.center, endPath.radius);
                    startPathEndPathAngleDiff = 0;
                    startToEndOffset = startToEndDistance;
                }
                else {
                    startPath.set(endPath.center, endPath.radius).reverse();
                    startPathEndPathAngleDiff = Math.PI;
                    startToEndOffset = startToEndDistance;
                }
            }
            if (startPath) {
                startPathEndPathAngleDiff !== null && startPathEndPathAngleDiff !== void 0 ? startPathEndPathAngleDiff : (startPathEndPathAngleDiff = Vec3Math.unitAngle(startPath.center, endPath.center));
                if (startPathEndPathAngleDiff <= GeoMath.ANGULAR_TOLERANCE) {
                    // The start and end paths are parallel. Check if continuing along the start path is the shortest path from the
                    // start to the end point. If so, then we will simply create a great-circle vector from the start to the end.
                    startToEndOffset !== null && startToEndOffset !== void 0 ? startToEndOffset : (startToEndOffset = endPath.angleAlong(start, end, Math.PI, GeoMath.ANGULAR_TOLERANCE));
                    if (startToEndOffset <= Math.PI) {
                        return this.circleVectorBuilder.build(vectors, index, endPath, start, end, flags, heading, isHeadingTrue);
                    }
                    else {
                        startTurnDirection !== null && startTurnDirection !== void 0 ? startTurnDirection : (startTurnDirection = endTurnDirection);
                        if (startTurnDirection !== endTurnDirection) {
                            endTurnDirection = endTurnDirection === 'left' ? 'right' : 'left';
                            endTurnCircle.reverse();
                        }
                        const startTurnCircle = FlightPathUtils.getTurnCircleStartingFromPath(start, startPath, turnRadiusRad, startTurnDirection, DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[2]);
                        const turnFlags = flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : exports.FlightPathVectorFlags.None);
                        return this.connectCirclesVectorBuilder.build(vectors, index, startTurnCircle, endTurnCircle, undefined, start, end, turnFlags, turnFlags, flags, heading, isHeadingTrue);
                    }
                }
                else if (startPathEndPathAngleDiff >= Math.PI - GeoMath.ANGULAR_TOLERANCE) {
                    // The start and end paths are antiparallel. We will execute a procedure turn that ends at the end point.
                    const startTurnSign = startTurnDirection === 'left' ? -1 : 1;
                    return this.procTurnVectorBuilder.build(vectors, index, start, startPath, end, endPath, startPath.bearingAt(start, Math.PI) + 45 * startTurnSign, turnRadius, startTurnDirection === 'left' ? 'right' : 'left', undefined, undefined, flags, includeTurnToCourseFlag, heading, isHeadingTrue);
                }
            }
            // For the rest of the method below, the start point is guaranteed to not lie on the end path.
            const startTurnCircle = DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[2];
            let isDone = false;
            do {
                let endTurnCircleIncludesStart;
                let isStartPathTangentToEndCircle;
                if (!startPath) {
                    // We can choose the start path and the start point does not lie on the end path. We will attempt to choose a
                    // direction for the start path such that the start path is tangent with the end turn.
                    startPath = DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[0];
                    const endTurnCenter = FlightPathUtils.getTurnCenterFromCircle(endTurnCircle, DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache[2]);
                    const startToEndTurnCenterDistance = Vec3Math.unitAngle(endTurnCenter, start);
                    if (Math.abs(startToEndTurnCenterDistance - turnRadiusRad) <= GeoMath.ANGULAR_TOLERANCE) {
                        // The start point lies on the end turn circle.
                        startPath.setAsGreatCircle(start, endTurnCircle);
                        endTurnCircleIncludesStart = true;
                        isStartPathTangentToEndCircle = true;
                    }
                    else if (Math.min(startToEndTurnCenterDistance, Math.PI - startToEndTurnCenterDistance) > turnRadiusRad) {
                        // The angle between the great-circle path from the start point to the center of the end turn and the two
                        // great-circle paths that are tangent to the end turn circle and include the start point.
                        const alpha = Math.asin(MathUtils.clamp(Math.sin(turnRadiusRad) / Math.sin(startToEndTurnCenterDistance), -1, 1));
                        startPath.setAsGreatCircle(start, endTurnCenter);
                        // Choose the great-circle tangent that proceeds in the direction from the start point to the tangent point
                        // with the end turn circle.
                        startPath.rotate(start, alpha * (endTurnDirection === 'left' ? 1 : -1), Math.PI);
                        endTurnCircleIncludesStart = false;
                        isStartPathTangentToEndCircle = true;
                    }
                    else {
                        // The start point lies in one of the areas that make it impossible to define a great-circle path that
                        // both includes the start point and is tangent to the end turn circle. Therefore, we will make the start
                        // path lie along the radial from the end turn center and the start point. The direction of the start path
                        // (toward or away from the end turn center) is chosen such that if the start point is close to the turn
                        // center, then the start path is directed away from the turn center, and if the start point is close to the
                        // antipode of the turn center, then the start path is directed toward the turn center.
                        if (startToEndTurnCenterDistance < turnRadiusRad) {
                            startPath.setAsGreatCircle(endTurnCenter, start);
                        }
                        else {
                            startPath.setAsGreatCircle(start, endTurnCenter);
                        }
                        endTurnCircleIncludesStart = false;
                        isStartPathTangentToEndCircle = false;
                    }
                }
                endTurnCircleIncludesStart !== null && endTurnCircleIncludesStart !== void 0 ? endTurnCircleIncludesStart : (endTurnCircleIncludesStart = endTurnCircle.includes(start));
                isStartPathTangentToEndCircle !== null && isStartPathTangentToEndCircle !== void 0 ? isStartPathTangentToEndCircle : (isStartPathTangentToEndCircle = DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[3]
                    // The circle that represents the centers of all geo circles of radius equal to the end turn circle radius
                    // that are tangent to the start path.
                    .set(startPath.center, Math.abs(endTurnCircle.radius - MathUtils.HALF_PI))
                    .includes(endTurnCircle.center));
                if (endTurnCircleIncludesStart && isStartPathTangentToEndCircle) {
                    // The start point lies on the end turn circle and the start path is tangent to the end turn circle. We will
                    // build a vector along the end turn circle from the start to the end point if the two points are not
                    // coincident.
                    if (Vec3Math.unitAngle(start, end) > GeoMath.ANGULAR_TOLERANCE) {
                        return this.circleVectorBuilder.build(vectors, index, endTurnCircle, start, end, flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : exports.FlightPathVectorFlags.None), heading, isHeadingTrue);
                    }
                    else {
                        return 0;
                    }
                }
                if (startTurnDirection === undefined) {
                    // If the start turn direction is not defined, then we will choose the turn direction such that the turn is
                    // toward the end point. If the end point is on the start path, then we will choose the start turn direction to
                    // be the same as the end turn direction.
                    const endDistanceFromStartPath = startPath.distance(end);
                    if (endDistanceFromStartPath < -GeoMath.ANGULAR_TOLERANCE) {
                        startTurnDirection = 'left';
                    }
                    else if (endDistanceFromStartPath > GeoMath.ANGULAR_TOLERANCE) {
                        startTurnDirection = 'right';
                    }
                    else {
                        startTurnDirection = endTurnDirection;
                    }
                }
                FlightPathUtils.getTurnCircleStartingFromPath(start, startPath, turnRadiusRad, startTurnDirection, startTurnCircle);
                if (startTurnDirection !== endTurnDirection && !isStartPathTangentToEndCircle) {
                    // If the start and end turn directions are not the same, then we need to check if the two turn circles are
                    // secant. If so, then no great-circle path can join the two turns (i.e. no great-circle path is tangent to both
                    // turn circles). If this is the case, then we will reverse the end turn direction - turn circles with the same
                    // direction are guaranteed to have at least one great-circle tangent to both of them (as long as they are not
                    // concentric).
                    const startTurnCenter = FlightPathUtils.getTurnCenterFromCircle(startTurnCircle, DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache[2]);
                    const endTurnCenter = FlightPathUtils.getTurnCenterFromCircle(endTurnCircle, DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache[3]);
                    if (Vec3Math.unitAngle(startTurnCenter, endTurnCenter) < 2 * turnRadiusRad - GeoMath.ANGULAR_TOLERANCE) {
                        endTurnDirection = startTurnDirection;
                        endTurnCircle.reverse();
                        // If we reversed the end turn direction, then we will continue the loop. The loop is guaranteed to iterate a
                        // maximum of two times since the directions of the start and end turns is guaranteed to be the same in the
                        // second iteration.
                    }
                    else {
                        isDone = true;
                    }
                }
                else {
                    isDone = true;
                }
            } while (!isDone);
            const turnFlags = flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : exports.FlightPathVectorFlags.None);
            return this.connectCirclesVectorBuilder.build(vectors, index, startTurnCircle, endTurnCircle, undefined, start, end, turnFlags, turnFlags, flags, heading, isHeadingTrue);
        }
    }
    DirectToJoinGreatCircleAtPointVectorBuilder.vec3Cache = ArrayUtils.create(5, () => Vec3Math.create());
    DirectToJoinGreatCircleAtPointVectorBuilder.geoCircleCache = ArrayUtils.create(4, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * Builds vectors representing paths connecting initial great-circle paths to final great-circle paths terminating at
     * defined end points.
     */
    class JoinGreatCircleToPointVectorBuilder {
        constructor() {
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.turnToJoinGreatCircleVectorBuilder = new TurnToJoinGreatCircleVectorBuilder();
            this.interceptGreatCircleToPointVectorBuilder = new InterceptGreatCircleToPointVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
        }
        /**
         * Builds a sequence of vectors representing a path from a defined start point and initial course that turns and
         * joins a great-circle path that terminates at a defined end point.
         *
         * This method will first attempt to connect the starting point and final path with a single constant-radius turn
         * of at least the minimum turn radius and in the desired direction that joins the final path before the end point.
         * If this is not possible, then what happens next depends on the `preferSingleTurn` argument:
         * * If it is `true`, then another path connecting the starting point and final path with a single constant-radius
         * turn will be computed - this path will respect the minimum turn radius but not necessarily the desired turn
         * direction.
         * * If it is `false`, then a path to intercept the final path at a 45-degree angle will be computed. If such a path
         * is not possible or if the intercept point lies after the end point, and `allowDirectFallback` is `true`, then a
         * direct path to the end point will be computed. If `allowDirectFallback` is `false` and all attempts to compute a
         * path have failed, then no vectors will be built.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param start The start point.
         * @param startPath A GeoCircle that defines the initial course. Must be a great circle and must include the start
         * point.
         * @param end The end point.
         * @param endPath A GeoCircle that defines the final course. Must be a great circle and must include the end point.
         * @param desiredTurnDirection The desired initial turn direction. If not defined, then the most efficient turn
         * direction that satisfies the constraints will be chosen.
         * @param minTurnRadius The minimum turn radius, in meters. Defaults to 0.
         * @param preferSingleTurn Whether to prefer flight path solutions that consist of a single constant-radius turn
         * from the initial to final course. Defaults to `false`.
         * @param allowDirectFallback Whether the computed path should fall back to a direct path to the end point if the
         * final path cannot be joined before the end point. Defaults to `true`.
         * @param intersection The point of intersection between the start and end paths that lies closest to the start
         * point. If not defined, then it will be calculated by this method.
         * @param flags The flags to set on all built vectors. Defaults to none (0).
         * @param includeTurnToCourseFlag Whether to include the `TurnToCourse` flag on the vectors that comprise turns.
         * Defaults to `true`.
         * @param includeDirectFlag Whether to include the `Direct` flag on all built vectors when falling back to a direct
         * path. Defaults to `true`.
         * @param includeInterceptFlag Whether to include the `InterceptCourse` flag on all built vectors when building an
         * intercept path. Defaults to `true`.
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         * @throws Error if `startPath` or `endPath` is not a great circle.
         */
        build(vectors, index, start, startPath, end, endPath, desiredTurnDirection, minTurnRadius, preferSingleTurn = false, allowDirectFallback = true, intersection, flags = 0, includeTurnToCourseFlag = true, includeDirectFlag = true, includeInterceptFlag = true, heading = null, isHeadingTrue = false) {
            if (!startPath.isGreatCircle() || !endPath.isGreatCircle()) {
                throw new Error('JoinGreatCircleToPointVectorBuilder::build(): start or end path is not a great circle');
            }
            let vectorIndex = index;
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, JoinGreatCircleToPointVectorBuilder.vec3Cache[0]);
            }
            if (!(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, JoinGreatCircleToPointVectorBuilder.vec3Cache[1]);
            }
            if (!intersection) {
                const intersections = JoinGreatCircleToPointVectorBuilder.intersectionCache;
                const solutionCount = startPath.intersection(endPath, intersections);
                if (solutionCount === 0) {
                    return 0;
                }
                // choose the intersection closest to the start point.
                intersection = Vec3Math.copy(Vec3Math.dot(intersections[0], start) > 0
                    ? intersections[0]
                    : intersections[1], JoinGreatCircleToPointVectorBuilder.vec3Cache[2]);
            }
            const turnFlags = flags | (includeTurnToCourseFlag ? exports.FlightPathVectorFlags.TurnToCourse : 0);
            // Calculate the relative directions of the start point, intersection point, and end point.
            const intersectionToStartDot = Vec3Math.dot(Vec3Math.cross(startPath.center, intersection, JoinGreatCircleToPointVectorBuilder.vec3Cache[3]), start);
            // positive -> start point lies after the intersection (with respect to the direction of start path)
            const intersectionToStartSign = intersectionToStartDot < -GeoMath.ANGULAR_TOLERANCE ? -1
                : intersectionToStartDot > GeoMath.ANGULAR_TOLERANCE ? 1 : 0;
            const intersectionToEndDot = Vec3Math.dot(Vec3Math.cross(endPath.center, intersection, JoinGreatCircleToPointVectorBuilder.vec3Cache[3]), end);
            // positive -> end point lies after the intersection (with respect to the direction of end path)
            const intersectionToEndSign = intersectionToEndDot < -GeoMath.ANGULAR_TOLERANCE ? -1
                : intersectionToEndDot > GeoMath.ANGULAR_TOLERANCE ? 1 : 0;
            const isEndForwardOfIntersection = intersectionToEndSign > 0;
            minTurnRadius !== null && minTurnRadius !== void 0 ? minTurnRadius : (minTurnRadius = 0);
            const minTurnRadiusRad = UnitType.METER.convertTo(minTurnRadius, UnitType.GA_RADIAN);
            const pathDot = Vec3Math.dot(startPath.center, endPath.center);
            const theta = Math.acos(MathUtils.clamp(-pathDot, -1, 1));
            const tanHalfTheta = Math.tan(theta / 2);
            const sinMinD = Math.tan(minTurnRadiusRad) / tanHalfTheta;
            // The along-track distance from the intersection point to the start/end of the minimum-radius turn from the start
            // path to the end path.
            const minD = Math.abs(sinMinD) <= 1
                ? Math.asin(sinMinD)
                : Infinity; // The turn radius is too large for any turn to join the start and end paths.
            const intersectionStartDistance = intersectionToStartSign === 0 ? 0 : Vec3Math.unitAngle(intersection, start);
            const intersectionEndDistance = intersectionToEndSign === 0 ? 0 : Vec3Math.unitAngle(intersection, end);
            const intersectionStartOffset = intersectionToStartSign * intersectionStartDistance;
            const intersectionEndOffset = intersectionToEndSign * intersectionEndDistance;
            const towardEndPointTurnDirection = startPath.encircles(end, false) ? 'left' : 'right';
            let needCalculateTwoTurnPath = false;
            let needCalculateOneTurnPath = false;
            if (isEndForwardOfIntersection) {
                if (desiredTurnDirection === undefined || desiredTurnDirection === towardEndPointTurnDirection) {
                    const isStartPastRequiredTurnStart = intersectionStartOffset > -minD;
                    const isEndBeforeRequiredTurnEnd = intersectionEndOffset < minD;
                    if (isStartPastRequiredTurnStart || isEndBeforeRequiredTurnEnd) {
                        // The minimum turn radius is too large to intercept the final path before the end point.
                        needCalculateTwoTurnPath = !preferSingleTurn;
                        needCalculateOneTurnPath = preferSingleTurn;
                    }
                    else {
                        // Make a single constant-radius turn connecting the start path with the end path. The turn will either start
                        // at the start point or end at the end point, depending on which is closer to the intersection point.
                        const turnRadius = UnitType.GA_RADIAN.convertTo(Math.atan(tanHalfTheta * Math.sin(Math.min(intersectionStartDistance, intersectionEndDistance))), UnitType.METER);
                        if (intersectionStartDistance <= intersectionEndDistance) {
                            // The turn starts at the start point.
                            vectorIndex += this.turnToJoinGreatCircleVectorBuilder.build(vectors, vectorIndex, start, startPath, endPath, turnRadius, turnFlags, heading, isHeadingTrue);
                            if (intersectionEndDistance - intersectionStartDistance > GeoMath.ANGULAR_TOLERANCE) {
                                const turnEnd = endPath.offsetDistanceAlong(intersection, intersectionStartDistance, JoinGreatCircleToPointVectorBuilder.vec3Cache[3], Math.PI);
                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, endPath, turnEnd, end, flags, heading, isHeadingTrue);
                            }
                        }
                        else {
                            // The turn ends at the end point.
                            let turnStart = start;
                            if (intersectionStartDistance - intersectionEndDistance > GeoMath.ANGULAR_TOLERANCE) {
                                turnStart = startPath.offsetDistanceAlong(intersection, -intersectionEndDistance, JoinGreatCircleToPointVectorBuilder.vec3Cache[3], Math.PI);
                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startPath, start, turnStart, flags, heading, isHeadingTrue);
                            }
                            vectorIndex += this.turnToJoinGreatCircleVectorBuilder.build(vectors, vectorIndex, turnStart, startPath, endPath, turnRadius, turnFlags, heading, isHeadingTrue);
                        }
                    }
                }
                else {
                    // The initial turn is away from the end point.
                    if (pathDot >= 0) {
                        // The start and end paths intersect at an angle <= 90 degrees. This means that for a turn away from the end
                        // point, the total flight path distance is minimized when the turn joins the start and end paths directly.
                        if (intersectionStartOffset <= minD) {
                            // The start point lies at or before the start point of the single constant-radius turn that minimizes the
                            // flight path distance. Therefore, we always want to use the single-turn solution.
                            needCalculateOneTurnPath = true;
                        }
                        else {
                            needCalculateTwoTurnPath = !preferSingleTurn;
                            needCalculateOneTurnPath = preferSingleTurn;
                        }
                    }
                    else {
                        needCalculateTwoTurnPath = !preferSingleTurn;
                        needCalculateOneTurnPath = preferSingleTurn;
                    }
                }
            }
            else {
                needCalculateTwoTurnPath = !preferSingleTurn;
                needCalculateOneTurnPath = preferSingleTurn;
            }
            let needDirectFallback = false;
            if (needCalculateTwoTurnPath) {
                const interceptFlag = includeInterceptFlag ? exports.FlightPathVectorFlags.InterceptCourse : 0;
                // Attempt to make a turn to intercept the end path at 45 degrees. At this point we are in fallback territory
                // so we won't honor the desired starting turn direction.
                const numInterceptVectors = this.interceptGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, start, startPath, minTurnRadius, undefined, JoinGreatCircleToPointVectorBuilder.INTERCEPT_ANGLE, end, endPath, minTurnRadius, turnFlags | interceptFlag, flags | interceptFlag, turnFlags | interceptFlag, heading, isHeadingTrue);
                if (numInterceptVectors === 0) {
                    needDirectFallback = allowDirectFallback;
                }
                else {
                    // The intercept vector builder will only build vectors up to the point where the intercept path turns and
                    // joins the end path. So we need to check if we need to add a vector to connect the intercept point to the end
                    // point.
                    vectorIndex += numInterceptVectors;
                    const lastVector = vectors[vectorIndex - 1];
                    const interceptEnd = GeoPoint.sphericalToCartesian(lastVector.endLat, lastVector.endLon, JoinGreatCircleToPointVectorBuilder.vec3Cache[5]);
                    if (Vec3Math.unitAngle(interceptEnd, end) > GeoMath.ANGULAR_TOLERANCE) {
                        vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, endPath, interceptEnd, end, flags, heading, isHeadingTrue);
                    }
                }
            }
            else if (needCalculateOneTurnPath) {
                // Make a single constant-radius turn from the start path to join the end path. We are guaranteed that the turn
                // starts after the intersection of the start and end paths (the only case where teht urn starts before the
                // intersection is handled above).
                if (minD > MathUtils.HALF_PI) {
                    // No amount of anticipation can provide a turn of the desired radius that joins the start and end paths.
                    needDirectFallback = allowDirectFallback;
                }
                else {
                    const minTurnEndOffset = Math.min(intersectionEndOffset, -minD);
                    const turnStartOffset = Math.max(-minTurnEndOffset, intersectionStartOffset);
                    const turnRadius = UnitType.GA_RADIAN.convertTo(Math.atan(tanHalfTheta * Math.sin(turnStartOffset)), UnitType.METER);
                    const turnStart = startPath.offsetDistanceAlong(intersection, turnStartOffset, JoinGreatCircleToPointVectorBuilder.vec3Cache[3]);
                    if (turnStartOffset - intersectionStartOffset > GeoMath.ANGULAR_TOLERANCE) {
                        vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startPath, start, turnStart, flags, heading, isHeadingTrue);
                    }
                    vectorIndex += this.turnToJoinGreatCircleVectorBuilder.build(vectors, vectorIndex, turnStart, startPath, endPath, turnRadius, turnFlags, heading, isHeadingTrue);
                    if (intersectionEndOffset + turnStartOffset > GeoMath.ANGULAR_TOLERANCE) {
                        const turnEnd = endPath.offsetDistanceAlong(intersection, -turnStartOffset, JoinGreatCircleToPointVectorBuilder.vec3Cache[4]);
                        vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, endPath, turnEnd, end, flags, heading, isHeadingTrue);
                    }
                }
            }
            if (needDirectFallback) {
                vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, start, startPath, end, minTurnRadius, desiredTurnDirection, flags, includeTurnToCourseFlag, includeDirectFlag, heading, isHeadingTrue);
            }
            return vectorIndex - index;
        }
    }
    JoinGreatCircleToPointVectorBuilder.INTERCEPT_ANGLE = 45; // degrees
    JoinGreatCircleToPointVectorBuilder.vec3Cache = ArrayUtils.create(6, () => Vec3Math.create());
    JoinGreatCircleToPointVectorBuilder.intersectionCache = [Vec3Math.create(), Vec3Math.create()];

    /**
     * Calculates flight path vectors for course to fix legs.
     */
    class CourseToFixLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of CourseToFixLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, true);
            this.vec3Cache = ArrayUtils.create(4, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(3, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(4, () => new GeoCircle(Vec3Math.create(), 0));
            this.intersectionCache = [Vec3Math.create(), Vec3Math.create()];
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.joinGreatCircleToPointVectorBuilder = new JoinGreatCircleToPointVectorBuilder();
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[0], leg.leg.fixIcao);
            leg.calculated.courseMagVar = terminatorPos === undefined ? 0 : this.getLegMagVar(leg.leg, terminatorPos);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            var _a, _b, _c, _d;
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[1], leg.leg.fixIcao);
            if (!terminatorPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const minTurnRadius = state.desiredTurnRadius.asUnit(UnitType.METER);
            if (state.isFallback && !state.isDiscontinuity && state.currentPosition.isValid() && state.currentCourse !== undefined) {
                // We are in a fallback state -> create a direct path to the end point.
                vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, state.currentCourse, terminatorPos, minTurnRadius, undefined, exports.FlightPathVectorFlags.Fallback);
                state.isFallback = false;
            }
            else {
                state.isFallback = false;
                const prevLeg = legs[calculateIndex - 1];
                const endCourse = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
                const endVec = terminatorPos.toCartesian(this.vec3Cache[1]);
                const endPath = this.geoCircleCache[0].setAsGreatCircle(terminatorPos, endCourse);
                if (state.isDiscontinuity || !state.currentPosition.isValid()) {
                    // ---- CASE A ----
                    // The leg begins in a discontinuity.
                    // Create a great-circle vector with a start point arbitrarily placed 5 NM from the terminator fix.
                    const startVec = endPath.offsetDistanceAlong(endVec, UnitType.NMILE.convertTo(-5, UnitType.GA_RADIAN), this.geoPointCache[2]);
                    if (options.calculateDiscontinuityVectors && state.currentPosition.isValid()) {
                        // We are configured to calculate discontinuity vectors. Build a path to the start point.
                        const startPath = state.currentCourse !== undefined
                            ? this.geoCircleCache[1].setAsGreatCircle(state.currentPosition, state.currentCourse)
                            : undefined;
                        vectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, startPath, startVec, endPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Discontinuity, true);
                    }
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, startVec, terminatorPos);
                }
                else {
                    const nextLeg = legs[calculateIndex + 1];
                    const startPoint = state.currentPosition;
                    const currentCourse = (_a = state.currentCourse) !== null && _a !== void 0 ? _a : startPoint.bearingTo(terminatorPos);
                    const startVec = startPoint.toCartesian(this.vec3Cache[0]);
                    const startPath = this.geoCircleCache[1].setAsGreatCircle(startPoint, currentCourse);
                    const startToEndPath = this.geoCircleCache[3].setAsGreatCircle(startVec, endVec);
                    const isStartEqualToEnd = startPoint.equals(terminatorPos);
                    const pathAngleDiff = Vec3Math.unitAngle(startPath.center, endPath.center);
                    // A great circle defining the threshold of the terminator fix - everything to the LEFT of (i.e. encircled by)
                    // this great circle is past the terminator fix as projected along the end path.
                    const threshold = this.geoCircleCache[2].setAsGreatCircle(endPath.center, endVec);
                    const isStartPastThreshold = threshold.encircles(startVec, false);
                    // 175 degrees
                    const areStartEndPathsAntiParallel = pathAngleDiff >= 3.05432619 - GeoMath.ANGULAR_TOLERANCE;
                    let isDone = false;
                    if (!areStartEndPathsAntiParallel && isStartPastThreshold) {
                        // ---- CASE B ----
                        // The start and end paths are not antiparallel and the start point is past the terminator threshold.
                        let shouldTryFallback = true;
                        const desiredTurnDirection = FlightPathUtils.getLegDesiredTurnDirection(leg.leg);
                        if (pathAngleDiff >= MathUtils.HALF_PI
                            && (desiredTurnDirection === undefined || (desiredTurnDirection === 'left') === startPath.encircles(endVec))) {
                            // The start path is either heading toward the terminator threshold or is parallel to it and the desired
                            // turn direction is not away from the end point. We now need to find the intersections between the start
                            // and end paths. There should be two intersections since both paths are great circles and they are not
                            // parallel or antiparallel.
                            const intersections = this.intersectionCache;
                            const solutionCount = startPath.intersection(endPath, intersections);
                            if (solutionCount === 2) {
                                // There are two general cases:
                                //
                                // 1. The end point lies past the intersection as measured along the end path.
                                // 2. The end point lies before the intersection as measured along the end path.
                                //
                                // In case 1, we can use the default algorithm for joining the start and end paths. Therefore there is
                                // nothing to do here; we just need to make sure the code falls through to Case E below.
                                //
                                // In case 2, we will try to join the start and end paths with a single constant radius turn. This will
                                // generate a "loop" where the path follows the start path initially away from the end point and then
                                // turns back onto the end path to head back to the end point. If that is not possible or the generated
                                // path is too long, we will fall through to Case E.
                                shouldTryFallback = false;
                                // Choose the intersection closest to the start point.
                                const intersection = Vec3Math.dot(intersections[0], startVec) > 0
                                    ? intersections[0]
                                    : intersections[1];
                                const intersectionToEndDot = Vec3Math.dot(Vec3Math.cross(endPath.center, intersection, this.vec3Cache[2]), endVec);
                                const isEndPastIntersection = intersectionToEndDot > GeoMath.ANGULAR_TOLERANCE;
                                if (!isEndPastIntersection) {
                                    vectorIndex += this.joinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, startVec, startPath, endVec, endPath, desiredTurnDirection, minTurnRadius, true, false, intersection);
                                    if (vectorIndex !== 0) {
                                        // Find all the great-circle vectors in the path. These are all guaranteed to be parallel to either
                                        // the start or end paths. If the total distance of all these vectors is greater than a maximum
                                        // threshold, erase the vectors and let the code fall through to Case E, which will generate a
                                        // shorter path.
                                        let distance = 0;
                                        for (let i = 0; i < vectorIndex; i++) {
                                            const vector = vectors[i];
                                            if (FlightPathUtils.isVectorGreatCircle(vector)) {
                                                distance += vector.distance;
                                            }
                                        }
                                        if (distance > 37040 /* 20 nautical miles */) {
                                            vectorIndex = 0;
                                        }
                                    }
                                    isDone = vectorIndex > 0;
                                }
                            }
                        }
                        if (shouldTryFallback
                            && !leg.leg.flyOver
                            && nextLeg
                            && !CourseToFixLegCalculator.FALLBACK_INELIGIBLE_LEG_TYPES.includes(nextLeg.leg.type)) {
                            // The leg does not end in a fly-over fix and the next leg is eligible for fallback -> end the current leg
                            // at the start point and set a fallback state.
                            // NOTE: At this point, state.currentPosition is guaranteed to be defined, so we don't have to bother
                            // with setting it.
                            (_b = state.currentCourse) !== null && _b !== void 0 ? _b : (state.currentCourse = currentCourse);
                            state.isFallback = true;
                            isDone = true;
                        }
                    }
                    if (!isDone) {
                        if (areStartEndPathsAntiParallel) {
                            // ---- CASE C ----
                            // The start and end paths are antiparallel. We need to execute a procedure turn to do a 180.
                            let desiredTurnDirection;
                            switch (leg.leg.turnDirection) {
                                // If the leg defines a turn direction, respect it.
                                case exports.LegTurnDirection.Left:
                                    desiredTurnDirection = 'left';
                                    break;
                                case exports.LegTurnDirection.Right:
                                    desiredTurnDirection = 'right';
                                    break;
                                default: {
                                    const endDistanceFromStartPath = startPath.distance(endVec);
                                    if (Math.abs(endDistanceFromStartPath) <= GeoMath.ANGULAR_TOLERANCE) {
                                        // If the end point lies on the start path, then we want to turn toward the end path after passing
                                        // the end point along the start path (defaulting to a right turn if the start and end paths are
                                        // exactly antiparallel).
                                        const cross = Vec3Math.cross(startPath.center, endVec, this.vec3Cache[2]);
                                        desiredTurnDirection = Vec3Math.dot(cross, endPath.center) > 0 ? 'left' : 'right';
                                    }
                                    else {
                                        // If the end point does not lie on the start path, then we want to turn toward the end point from
                                        // the start path.
                                        desiredTurnDirection = endDistanceFromStartPath < 0 ? 'left' : 'right';
                                    }
                                }
                            }
                            vectorIndex += this.procTurnVectorBuilder.build(vectors, vectorIndex, startVec, startPath, endVec, endPath, currentCourse + (desiredTurnDirection === 'left' ? -45 : 45), minTurnRadius, desiredTurnDirection, currentCourse, endCourse);
                            // The procedure turn vector builder will only build vectors up to the point where the  procedure turn
                            // intercepts the end path. So we need to check if we need to add a vector to connect the intercept point
                            // to the end point.
                            if (vectorIndex > 0) {
                                const lastVector = vectors[vectorIndex - 1];
                                const interceptVec = GeoPoint.sphericalToCartesian(lastVector.endLat, lastVector.endLon, this.vec3Cache[2]);
                                const interceptToEndDistance = endPath.angleAlong(interceptVec, endVec, Math.PI);
                                if (interceptToEndDistance > 1e-5 && interceptToEndDistance < Math.PI + GeoMath.ANGULAR_TOLERANCE) { // ~60 meter tolerance
                                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, this.geoPointCache[2].set(lastVector.endLat, lastVector.endLon), terminatorPos, -endCourse);
                                }
                            }
                        }
                        else if (endPath.angleAlong(startVec, endVec, Math.PI, GeoMath.ANGULAR_TOLERANCE) < Math.PI + GeoMath.ANGULAR_TOLERANCE
                            && (pathAngleDiff <= GeoMath.ANGULAR_TOLERANCE
                                || (!isStartEqualToEnd
                                    && (Vec3Math.dot(startToEndPath.center, endPath.center) >= 0.996194698 // 5 degrees
                                        || (((_c = prevLeg === null || prevLeg === void 0 ? void 0 : prevLeg.calculated) === null || _c === void 0 ? void 0 : _c.flightPath.length) && endPath.includes(startVec, UnitType.METER.convertTo(10, UnitType.GA_RADIAN))))))) {
                            // ---- CASE D ----
                            // The start and end paths are parallel, so we can just connect the start and end with a great-circle path.
                            // Or the start point lies on the final course path (within a generous tolerance) and the previous leg has
                            // at least one calculated vector. In this case we will simply create a track from the start to end and let
                            // turn anticipation handle the initial turn into the final course.
                            if (!isStartEqualToEnd) {
                                vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, startPoint, terminatorPos, -endCourse);
                            }
                        }
                        else {
                            // ---- CASE E ----
                            // The default case. We will attempt to join the start and end paths with a single constant-radius turn
                            // toward the end point. If that is not possible, we will fall back to using two constant-radius turns. If
                            // that is not possible, we will fall back to a direct path from the start point to the end point.
                            const desiredTurnDirection = FlightPathUtils.getLegDesiredTurnDirection(leg.leg);
                            vectorIndex += this.joinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, startVec, startPath, endVec, endPath, desiredTurnDirection, minTurnRadius);
                            const lastVector = vectors[vectorIndex - 1];
                            if (lastVector !== undefined
                                && !leg.leg.flyOver
                                && nextLeg
                                && !CourseToFixLegCalculator.FALLBACK_INELIGIBLE_LEG_TYPES.includes(nextLeg.leg.type)) {
                                const lastVectorEndVec = GeoPoint.sphericalToCartesian(lastVector.endLat, lastVector.endLon, this.vec3Cache[2]);
                                const lastVectorEndPath = FlightPathUtils.getGreatCircleTangentToVector(lastVectorEndVec, lastVector, this.geoCircleCache[3]);
                                const lastVectorCourseDiff = Vec3Math.unitAngle(lastVectorEndPath.center, endPath.center);
                                if (lastVectorCourseDiff > 0.0174533 /* 1 degree */) {
                                    // We are allowed to use a fallback path which does not end at the defined terminator fix and a fallback
                                    // direct path was calculated. We need to determine if we should end the direct-to path early if it
                                    // crosses past the terminator threshold or remove it entirely and end the leg immediately at the start
                                    // point.
                                    const minTurnRadiusRad = UnitType.METER.convertTo(minTurnRadius, UnitType.GA_RADIAN);
                                    let useImmediateFallback = false;
                                    let startTurnCircle;
                                    let startTurnEnd;
                                    // The direct path can consist of either a single turn vector, a single great-circle vector, or a
                                    // starting turn vector followed by a great-circle vector.
                                    if (FlightPathUtils.isVectorGreatCircle(lastVector)) {
                                        if (vectorIndex < 2) {
                                            // The direct path has a single great-circle vector. If the direct-to course differs from the end
                                            // course by more than 90 degrees, then the entire direct-to path is past the terminator threshold.
                                            // In that case we will end this leg at the start point and set a fallback state. If the direct
                                            // path course is within 90 degrees of the end course, then the path must be entirely behind the
                                            // threshold. In that case we will leave the path in place.
                                            if (lastVectorCourseDiff > MathUtils.HALF_PI) {
                                                useImmediateFallback = true;
                                            }
                                        }
                                        else {
                                            // The direct path consists of a starting turn followed by a great-circle vector. If the course of
                                            // the great-circle vector differs from the end course by more than 90 degrees, then we need to
                                            // deal with the possibility that the direct path starts behind the terminator threshold and then
                                            // crosses past the threshold. If the courses differ by 90 or degrees or less, then we will leave
                                            // the direct path in place.
                                            if (lastVectorCourseDiff > MathUtils.HALF_PI) {
                                                if (isStartPastThreshold) {
                                                    // If the start point is past the terminator threshold, then the entire direct path is
                                                    // guaranteed to be past the terminator threshold. Therefore we will end this leg at the start
                                                    // point and set a fallback state.
                                                    useImmediateFallback = true;
                                                }
                                                else {
                                                    // If the start point is not past the terminator threshold, then that means at some point the
                                                    // direct path (specifically the starting turn) must cross the threshold. Therefore we will
                                                    // trigger the evaluation code below to find out where we need to end the path early as it
                                                    // crosses the threshold.
                                                    const startTurnVector = vectors[vectorIndex - 2];
                                                    startTurnCircle = FlightPathUtils.setGeoCircleFromVector(startTurnVector, this.geoCircleCache[3]);
                                                    startTurnEnd = GeoPoint.sphericalToCartesian(startTurnVector.endLat, startTurnVector.endLon, this.vec3Cache[3]);
                                                }
                                            }
                                        }
                                    }
                                    else {
                                        // The direct path is a single turn vector.
                                        if (isStartPastThreshold) {
                                            // If the start point is past the terminator threshold, we will end this leg at the start point and
                                            // set a fallback state.
                                            useImmediateFallback = true;
                                        }
                                        else {
                                            // If the start point is behind the terminator threshold, then it is possible the turn crosses past
                                            // the threshold before it ends. Therefore we will trigger the evaluation code below to find out if
                                            // we need to end the path early as it crosses the threshold.
                                            // If the direct course calculation produced only a single turn vector, it possibly reduced the radius
                                            // of the starting turn below the minimum radius in order to build a valid path to the terminator.
                                            // We always want the starting turn to respect the minimum turn radius, so we will define it ourselves.
                                            startTurnCircle = FlightPathUtils.getTurnCircleStartingFromPath(startVec, startPath, minTurnRadiusRad, desiredTurnDirection !== null && desiredTurnDirection !== void 0 ? desiredTurnDirection : (startPath.encircles(endVec) ? 'left' : 'right'), this.geoCircleCache[3]);
                                            // If the direct course turn radius was reduced, then the terminator fix lies inside the starting turn
                                            // circle of minimum radius. Therefore, the turn technically never ends because there is no point on
                                            // the turn circle that either includes the terminator fix or is tangent to a great-circle path which
                                            // includes the terminator fix.
                                            if (Math.min(lastVector.radius, Math.PI - lastVector.radius) >= minTurnRadiusRad - GeoMath.ANGULAR_TOLERANCE) {
                                                startTurnEnd = GeoPoint.sphericalToCartesian(lastVector.endLat, lastVector.endLon, this.vec3Cache[3]);
                                            }
                                        }
                                    }
                                    if (startTurnCircle !== undefined) {
                                        // Find the intersections of the direct path starting turn circle with the terminator threshold.
                                        const intersections = this.intersectionCache;
                                        const intersectionCount = threshold.intersection(startTurnCircle, intersections);
                                        // If the starting turn is tangent to the threshold, then the entire turn must be behind the threshold
                                        // because we are guaranteed that the start point is behind the threshold if we made it into this case.
                                        // Therefore, we only care about starting turns that are secant to the threshold.
                                        if (intersectionCount === 2) {
                                            // Because the start point is guaranteed to be behind the threshold, the next intersection of the
                                            // starting turn circle with the threshold will take the path past the threshold.
                                            const thresholdCrossing = intersections[0];
                                            const thresholdCrossingAngle = startTurnCircle.angleAlong(startVec, thresholdCrossing, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                                            if (startTurnEnd === undefined
                                                || startTurnCircle.angleAlong(startVec, startTurnEnd, Math.PI, GeoMath.ANGULAR_TOLERANCE) > thresholdCrossingAngle + GeoMath.ANGULAR_TOLERANCE) {
                                                // The starting turn crosses the terminator threshold before the end of the turn (or the turn has
                                                // no end) -> end the turn at the crossing point and set the fallback state.
                                                vectorIndex = 0;
                                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, startTurnCircle, startVec, thresholdCrossing, exports.FlightPathVectorFlags.TurnToCourse | exports.FlightPathVectorFlags.Fallback);
                                                state.isFallback = true;
                                            }
                                        }
                                    }
                                    if (useImmediateFallback) {
                                        vectorIndex = 0;
                                        // NOTE: At this point, state.currentPosition is guaranteed to be defined, so we don't have to bother
                                        // with setting it.
                                        (_d = state.currentCourse) !== null && _d !== void 0 ? _d : (state.currentCourse = currentCourse);
                                        state.isFallback = true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            vectors.length = vectorIndex;
            const lastVector = vectors[vectorIndex - 1];
            if (lastVector !== undefined) {
                state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            }
            state.isDiscontinuity = false;
        }
    }
    CourseToFixLegCalculator.FALLBACK_INELIGIBLE_LEG_TYPES = [
        exports.LegType.AF,
        exports.LegType.RF,
        exports.LegType.PI
    ];

    /**
     * Calculates flight path vectors for direct to fix legs.
     */
    class DirectToFixLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of DirectToFixLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, true);
            this.geoPointCache = ArrayUtils.create(4, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            var _a;
            const leg = legs[calculateIndex];
            leg.calculated.courseMagVar = (_a = this.getTerminatorMagVar(leg.leg)) !== null && _a !== void 0 ? _a : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[1], leg.leg.fixIcao);
            if (!terminatorPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            // Only build vectors if...
            if (
            // ... the current flight path position is defined...
            state.currentPosition.isValid()
                // ... and the leg is of non-zero length.
                && !state.currentPosition.equals(terminatorPos)) {
                // If the leg starts in a discontinuity, then only build vectors if we are configured to calculate discontinuity
                // vectors.
                if (options.calculateDiscontinuityVectors || !state.isDiscontinuity) {
                    const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None)
                        | (state.isFallback && state.currentCourse !== undefined ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None);
                    const startPoint = this.geoPointCache[0].set(state.currentPosition);
                    let initialCourse;
                    if (leg.leg.course !== 0) {
                        // If a course is defined on the leg, then honor it.
                        initialCourse = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, startPoint);
                    }
                    else {
                        // If a course is not defined on the leg, then try to get the initial course from the end of the previous leg,
                        // which should be stored as the state's current course.
                        if (state.currentCourse === undefined) {
                            // If the state's current course is not defined, then attempt to select an initial course from the definition
                            // of the previous leg.
                            const prevLeg = legs[calculateIndex - 1];
                            if (prevLeg) {
                                initialCourse = this.getLegTrueCourse(prevLeg.leg);
                            }
                            if (initialCourse === undefined) {
                                // If we can't select an initial course from the previous leg, then default to the course that puts us on
                                // a great-circle path from the start point to the terminator fix.
                                initialCourse = startPoint.bearingTo(terminatorPos);
                                // If the calculated course is NaN (only happens when the start and end points are coincident or antipodal),
                                // then just arbitrarily use true north.
                                if (isNaN(initialCourse)) {
                                    initialCourse = 0;
                                }
                            }
                        }
                        else {
                            initialCourse = state.currentCourse;
                        }
                    }
                    const startPath = this.geoCircleCache[0].setAsGreatCircle(startPoint, initialCourse);
                    vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, startPoint, startPath, terminatorPos, state.desiredTurnRadius.asUnit(UnitType.METER), FlightPathUtils.getLegDesiredTurnDirection(leg.leg), flags);
                }
            }
            vectors.length = vectorIndex;
            const hasInvalidStart = state.isDiscontinuity || !state.currentPosition.isValid();
            state.currentPosition.set(terminatorPos);
            if (hasInvalidStart) {
                state.currentCourse = undefined;
            }
            else if (vectorIndex > 0) {
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(vectors[vectorIndex - 1]);
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for discontinuity legs.
     */
    class DiscontinuityLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of DiscontinuityLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            legs[calculateIndex].calculated.courseMagVar = 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            legs[calculateIndex].calculated.flightPath.length = 0;
            state.isDiscontinuity = true;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for legs that intercept a DME circle from a reference fix.
     */
    class DmeInterceptLegCalculator extends CircleInterceptLegCalculator {
        /**
         * Creates a new instance of DmeInterceptLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         * @param isHeadingLeg Whether the calculator calculates flight plan legs flown with constant heading.
         */
        constructor(facilityCache, isHeadingLeg) {
            super(facilityCache, isHeadingLeg);
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined) {
                let position;
                if (this.isHeadingLeg && calculateIndex === activeLegIndex && state.planePosition.isValid()) {
                    position = state.planePosition;
                }
                else if (!state.isDiscontinuity && state.currentPosition.isValid()) {
                    position = state.currentPosition;
                }
                if (position) {
                    magVar = MagVar.get(position);
                }
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        getPathToInterceptInfo(legs, calculateIndex, activeLegIndex, state, out) {
            const leg = legs[calculateIndex];
            const dmeCenter = this.getPositionFromIcao(leg.leg.originIcao, this.geoPointCache[0]);
            if (dmeCenter) {
                out.circle.set(dmeCenter, UnitType.METER.convertTo(leg.leg.distance, UnitType.GA_RADIAN));
                Vec3Math.set(NaN, NaN, NaN, out.start);
                Vec3Math.set(NaN, NaN, NaN, out.end);
                return out;
            }
            else {
                return undefined;
            }
        }
    }

    /**
     * Calculates flight path vectors for fix to DME legs.
     */
    class FixToDmeLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of FixToDmeLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, true);
            this.vec3Cache = ArrayUtils.create(2, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));
            this.intersectionCache = [Vec3Math.create(), Vec3Math.create()];
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const startFacility = this.facilityCache.get(leg.leg.fixIcao);
            leg.calculated.courseMagVar = startFacility === undefined ? 0 : this.getLegMagVar(leg.leg, startFacility);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            const dmeFacility = this.facilityCache.get(leg.leg.originIcao);
            if (!originPos || !dmeFacility) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const path = this.geoCircleCache[0].setAsGreatCircle(originPos, course);
            const dmeCircle = this.geoCircleCache[1].set(dmeFacility, UnitType.METER.convertTo(leg.leg.distance, UnitType.GA_RADIAN));
            const originVec = originPos.toCartesian(this.vec3Cache[0]);
            const interceptVec = this.vec3Cache[1];
            const intersections = this.intersectionCache;
            const numIntersections = path.intersection(dmeCircle, intersections);
            if (numIntersections === 0) {
                // The path along the leg's defined course does not intercept the DME circle -> define the intercept to be the
                // closest point on the DME circle to the initial fix.
                dmeCircle.closest(originVec, interceptVec);
            }
            else {
                // The path along the leg's defined course intercepts the DME circle -> choose the first intercept when
                // proceeding along the path from the initial fix.
                const intersectionIndex = (numIntersections === 1 || dmeCircle.encircles(originVec)) ? 0 : 1;
                Vec3Math.copy(intersections[intersectionIndex], interceptVec);
            }
            if ((!state.isDiscontinuity || options.calculateDiscontinuityVectors)
                && state.isFallback && state.currentPosition.isValid() && state.currentCourse !== undefined) {
                // We are starting in a fallback state and the current position is defined, so we will path a direct course to
                // the leg end point.
                const flags = exports.FlightPathVectorFlags.Fallback
                    | (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None);
                vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, state.currentCourse, interceptVec, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, flags);
            }
            else {
                let startVec;
                // If we are starting in a discontinuity and we are configured to calculate discontinuity vectors, then we will
                // build a path to the start point.
                if (!state.currentPosition.isValid()) {
                    // If the current flight path position is not valid, then we will start at the leg origin.
                    startVec = originVec;
                }
                else if (state.isDiscontinuity) {
                    // If we are starting in a discontinuity, then we will start at the leg origin. If we are configured to
                    // calculate discontinuity vectors, then we will also build a path to the origin point.
                    if (options.calculateDiscontinuityVectors && state.currentPosition.isValid()) {
                        const startPath = state.currentCourse !== undefined
                            ? this.geoCircleCache[1].setAsGreatCircle(state.currentPosition, state.currentCourse)
                            : undefined;
                        vectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, startPath, originPos, path, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Discontinuity, true);
                    }
                    startVec = originVec;
                }
                else {
                    startVec = state.currentPosition.toCartesian(this.vec3Cache[0]);
                }
                // Create a great-circle path from the start point to the end point.
                if (Vec3Math.unitAngle(startVec, interceptVec) > GeoMath.ANGULAR_TOLERANCE) {
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, startVec, interceptVec, course);
                }
            }
            vectors.length = vectorIndex;
            const hasInvalidStart = state.isDiscontinuity || !state.currentPosition.isValid();
            state.currentPosition.setFromCartesian(interceptVec);
            if (hasInvalidStart) {
                state.currentCourse = undefined;
            }
            else if (vectorIndex > 0) {
                const lastVector = vectors[vectorIndex - 1];
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Builds vectors representing paths connecting initial great circle paths to final great circle paths via a turn
     * starting at the start point and a turn ending at the end point, connected by a great-circle path.
     */
    class TurnToJoinGreatCircleAtPointVectorBuilder {
        constructor() {
            this.connectCirclesVectorBuilder = new ConnectCirclesVectorBuilder();
        }
        /**
         * Builds a sequence of vectors representing a path from a defined start point and initial course which turns and
         * connects with another turn via a great-circle path to terminate at a defined end point and final course.
         * @param vectors The flight path vector array to which to add the vectors.
         * @param index The index in the array at which to add the vectors.
         * @param start The start point.
         * @param startPath A GeoCircle that defines the initial course. Must be a great circle.
         * @param startTurnRadius The radius of the initial turn, in meters.
         * @param startTurnDirection The direction of the initial turn.
         * @param end The end point.
         * @param endPath A GeoCircle that defines the final course. Must be a great circle.
         * @param endTurnRadius The radius of the final turn, in meters.
         * @param endTurnDirection The direction of the final turn.
         * @param startTurnVectorFlags The flags to set on the initial turn vector. Defaults to none (0).
         * @param endTurnVectorFlags The flags to set on the final turn vector. Defaults to none (0).
         * @param connectVectorFlags The flags to set on the vector along the great-circle path connecting the turns.
         * Defaults to none (0).
         * @param heading The heading-to-fly to assign to all built vectors, in degrees, or `null` if no heading is to be
         * assigned. Defaults to `null`.
         * @param isHeadingTrue Whether the heading-to-fly assigned to built vectors is relative to true north instead of
         * magnetic north. Defaults to `false`.
         * @returns The number of vectors that were built and added to the array.
         * @throws Error if either the start or end path is not a great circle.
         */
        build(vectors, index, start, startPath, startTurnRadius, startTurnDirection, end, endPath, endTurnRadius, endTurnDirection, startTurnVectorFlags = 0, endTurnVectorFlags = 0, connectVectorFlags = 0, heading = null, isHeadingTrue = false) {
            if (!startPath.isGreatCircle() || !endPath.isGreatCircle()) {
                throw new Error('TurnToJoinGreatCircleAtPointVectorBuilder::build(): start or end path is not a great circle');
            }
            if (!(start instanceof Float64Array)) {
                start = GeoPoint.sphericalToCartesian(start, TurnToJoinGreatCircleAtPointVectorBuilder.vec3Cache[0]);
            }
            if (!(end instanceof Float64Array)) {
                end = GeoPoint.sphericalToCartesian(end, TurnToJoinGreatCircleAtPointVectorBuilder.vec3Cache[1]);
            }
            const startTurnRadiusRad = UnitType.METER.convertTo(startTurnRadius, UnitType.GA_RADIAN);
            const startTurnOffsetPath = TurnToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[0].setAsGreatCircle(start, startPath.center);
            const startTurnCircleRadiusRad = startTurnDirection === 'left' ? startTurnRadiusRad : Math.PI - startTurnRadiusRad;
            const startTurnCircleCenter = startTurnOffsetPath.offsetDistanceAlong(start, startTurnCircleRadiusRad, TurnToJoinGreatCircleAtPointVectorBuilder.vec3Cache[3], Math.PI);
            const startTurnCircle = TurnToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[0].set(startTurnCircleCenter, startTurnCircleRadiusRad);
            const endTurnRadiusRad = UnitType.METER.convertTo(endTurnRadius, UnitType.GA_RADIAN);
            const endTurnOffsetPath = TurnToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[1].setAsGreatCircle(end, endPath.center);
            const endTurnCircleRadiusRad = endTurnDirection === 'left' ? endTurnRadiusRad : Math.PI - endTurnRadiusRad;
            const endTurnCircleCenter = endTurnOffsetPath.offsetDistanceAlong(end, endTurnCircleRadiusRad, TurnToJoinGreatCircleAtPointVectorBuilder.vec3Cache[3], Math.PI);
            const endTurnCircle = TurnToJoinGreatCircleAtPointVectorBuilder.geoCircleCache[1].set(endTurnCircleCenter, endTurnCircleRadiusRad);
            return this.connectCirclesVectorBuilder.build(vectors, index, startTurnCircle, endTurnCircle, undefined, start, end, startTurnVectorFlags, endTurnVectorFlags, connectVectorFlags, heading, isHeadingTrue);
        }
    }
    TurnToJoinGreatCircleAtPointVectorBuilder.vec3Cache = ArrayUtils.create(5, () => Vec3Math.create());
    TurnToJoinGreatCircleAtPointVectorBuilder.geoCircleCache = ArrayUtils.create(3, () => new GeoCircle(Vec3Math.create(), 0));

    /**
     * Calculates flight path vectors for fix to manual termination legs.
     */
    class FixToManualLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of FixToManualLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.vec3Cache = ArrayUtils.create(3, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.interceptGreatCircleToPointVectorBuilder = new InterceptGreatCircleToPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            leg.calculated.courseMagVar = originPos === undefined ? 0 : this.getLegMagVar(leg.leg, originPos);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            var _a;
            const leg = legs[calculateIndex];
            const vectors = legs[calculateIndex].calculated.flightPath;
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            if (!originPos) {
                vectors.length = 0;
                // There is a discontinuity at the end of FM legs.
                state.isDiscontinuity = true;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const distance = UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN);
            if (state.currentPosition.isValid()
                && (!state.isDiscontinuity || options.calculateDiscontinuityVectors)
                && (state.isDiscontinuity || (state.isFallback && state.currentCourse !== undefined))) {
                // We are in a discontinuity state and are configured to calculate discontinuity vectors or we are in a fallback
                // state. We need to compute a path to connect the current position and course to the desired course from the
                // origin fix.
                const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None)
                    | (state.isFallback && state.currentCourse !== undefined ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None);
                const originVec = originPos.toCartesian(this.vec3Cache[0]);
                // If the current course is not defined, then set it to the course from the current position to the leg origin.
                if (state.currentCourse === undefined) {
                    state.currentCourse = state.currentPosition.bearingTo(originPos);
                    // If the current position is coincident with or antipodal to the leg origin, then set the current course to
                    // the leg course.
                    if (isNaN(state.currentCourse)) {
                        state.currentCourse = course;
                    }
                }
                const initialPath = this.geoCircleCache[0].setAsGreatCircle(state.currentPosition, state.currentCourse);
                const finalPath = this.geoCircleCache[1].setAsGreatCircle(originPos, course);
                const initialFinalPathAngle = Vec3Math.unitAngle(initialPath.center, finalPath.center);
                if (initialFinalPathAngle >= Math.PI - GeoMath.ANGULAR_TOLERANCE) {
                    // If the initial path and the final path are antiparallel, then we will path a procedure turn to do a 180.
                    vectorIndex += this.procTurnVectorBuilder.build(vectors, vectorIndex, state.currentPosition, initialPath, originVec, finalPath, state.currentCourse + 45, state.desiredCourseReversalTurnRadius.asUnit(UnitType.METER), undefined, state.currentCourse, course, flags | exports.FlightPathVectorFlags.CourseReversal, true);
                }
                else if (initialFinalPathAngle > GeoMath.ANGULAR_TOLERANCE) {
                    const interceptFlags = flags | exports.FlightPathVectorFlags.InterceptCourse;
                    const turnFlags = interceptFlags | exports.FlightPathVectorFlags.TurnToCourse;
                    vectorIndex += this.interceptGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, initialPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, 45, finalPath.offsetAngleAlong(originVec, MathUtils.HALF_PI, this.vec3Cache[1], Math.PI), finalPath, state.desiredTurnRadius.asUnit(UnitType.METER), turnFlags, interceptFlags, turnFlags);
                }
                let isOnFinalPath = false;
                if (initialFinalPathAngle <= GeoMath.ANGULAR_TOLERANCE) {
                    // We were already on the final path at the start of the leg.
                    isOnFinalPath = true;
                }
                else if (vectorIndex > 0) {
                    const lastVector = vectors[vectorIndex - 1];
                    state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                    state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
                    // To check if we are on the final path, first confirm that the last vector ends on the final path. Then,
                    // confirm that the great circle tangent to the last vector where the vector ends is parallel to the final
                    // path.
                    const lastVectorEndVec = state.currentPosition.toCartesian(this.vec3Cache[1]);
                    if (finalPath.includes(lastVectorEndVec)) {
                        const tangentCircleNormal = Vec3Math.normalize(Vec3Math.cross(Vec3Math.cross(lastVectorEndVec, Vec3Math.set(lastVector.centerX, lastVector.centerY, lastVector.centerZ, this.vec3Cache[2]), this.vec3Cache[2]), lastVectorEndVec, this.vec3Cache[2]), this.vec3Cache[2]);
                        // Angular difference <= 1e-6 radians
                        isOnFinalPath = Vec3Math.dot(tangentCircleNormal, finalPath.center) >= 0.9999999999995;
                    }
                }
                if (isOnFinalPath) {
                    // If we are on the final path, then check if we are behind the origin position. If we are, then we need to
                    // ensure we travel along the final path to the origin before continuing along the final path for the desired
                    // distance.
                    const startVec = state.currentPosition.toCartesian(this.vec3Cache[1]);
                    let distanceToOrigin = finalPath.distanceAlong(startVec, originVec, Math.PI, GeoMath.ANGULAR_TOLERANCE);
                    if (distanceToOrigin >= Math.PI) {
                        distanceToOrigin = 0;
                    }
                    vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, finalPath, startVec, finalPath.offsetDistanceAlong(startVec, distance + distanceToOrigin, this.vec3Cache[2], Math.PI));
                }
                else {
                    // If we are not on the final path, then we will just turn toward the prescribed course and fly for the desired
                    // distance along that course.
                    if (MathUtils.angularDistanceDeg(state.currentCourse, course, 0) > 1) {
                        vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), (_a = FlightPathUtils.getShortestTurnDirection(state.currentCourse, course)) !== null && _a !== void 0 ? _a : 'right', state.currentCourse, course, exports.FlightPathVectorFlags.Fallback | exports.FlightPathVectorFlags.TurnToCourse);
                        if (vectorIndex > 0) {
                            const lastVector = vectors[vectorIndex - 1];
                            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                        }
                        state.currentCourse = course;
                    }
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, state.currentPosition, course, distance, exports.FlightPathVectorFlags.Fallback);
                }
            }
            else {
                vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, originPos, course, distance);
            }
            vectors.length = vectorIndex;
            if (vectorIndex > 0) {
                const lastVector = vectors[vectorIndex - 1];
                state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            }
            // There is a discontinuity at the end of FM legs.
            state.isDiscontinuity = true;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for heading to manual termination legs.
     */
    class HeadingToManualLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of HeadingToManualLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.vec3Cache = ArrayUtils.create(1, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined) {
                let position;
                if (calculateIndex === activeLegIndex && state.planePosition.isValid()) {
                    position = state.planePosition;
                }
                else if (!state.isDiscontinuity && state.currentPosition.isValid()) {
                    position = state.currentPosition;
                }
                if (position) {
                    magVar = MagVar.get(position);
                }
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            var _a, _b;
            const leg = legs[calculateIndex];
            const vectors = legs[calculateIndex].calculated.flightPath;
            const heading = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const isHeadingTrue = leg.leg.trueDegrees;
            const distance = UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN);
            let course = heading;
            if (state.planeWindSpeed.number > 0) {
                course = NavMath.headingToGroundTrack(heading, state.planeTrueAirspeed.asUnit(UnitType.KNOT), state.planeWindDirection, state.planeWindSpeed.asUnit(UnitType.KNOT));
                if (isNaN(course)) {
                    course = heading;
                }
            }
            let startAtPlanePos = false;
            let retainOldVectors = false;
            const isActiveLeg = calculateIndex === activeLegIndex;
            if (isActiveLeg && state.planePosition.isValid()) {
                // If the leg to calculate is the active leg and we know the airplane's current position, then we should start
                // the leg path at the airplane's current position. The exception to this is if vectors have been previously
                // calculated and include an initial turn and the airplane's current position puts it within a certain
                // cross-track distance of the turn and before the end of the turn. In this case, we will retain the previously
                // calculated vectors.
                startAtPlanePos = true;
                if (vectors.length > 0) {
                    const firstVector = vectors[0];
                    if (!FlightPathUtils.isVectorGreatCircle(firstVector)) {
                        const firstVectorCircle = FlightPathUtils.setGeoCircleFromVector(firstVector, this.geoCircleCache[0]);
                        const planePosVec = state.planePosition.toCartesian(this.vec3Cache[0]);
                        const xtk = Math.abs(firstVectorCircle.distance(planePosVec));
                        if (xtk < UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN)) {
                            const alongVectorNormDistance = FlightPathUtils.getAlongArcNormalizedDistance(firstVectorCircle, this.geoPointCache[0].set(firstVector.startLat, firstVector.startLon), this.geoPointCache[1].set(firstVector.endLat, firstVector.endLon), planePosVec);
                            if (alongVectorNormDistance < 1) {
                                startAtPlanePos = false;
                                retainOldVectors = true;
                            }
                        }
                    }
                }
                if (startAtPlanePos) {
                    state.currentPosition.set(state.planePosition);
                    state.currentCourse = course;
                }
            }
            if (retainOldVectors) {
                // We need to ensure the flight path state is updated to reflect the end of the leg. Note that retainOldVectors
                // can only be true if the leg has at least one calculated vector.
                FlightPathUtils.getLegFinalPosition(leg.calculated, state.currentPosition);
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(vectors[vectors.length - 1]);
                state.isDiscontinuity = true;
                state.isFallback = false;
                return;
            }
            if (!state.currentPosition.isValid() || state.isDiscontinuity) {
                vectors.length = 0;
                // There is a discontinuity at the end of VM legs.
                state.isDiscontinuity = true;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            if (!startAtPlanePos) {
                // If we are not starting the path at the plane position, then calculate an initial turn toward the desired
                // course.
                if (state.currentCourse !== undefined && MathUtils.angularDistanceDeg(state.currentCourse, course, 0) > 5) {
                    const turnDirection = (_b = (_a = FlightPathUtils.getLegDesiredTurnDirection(leg.leg)) !== null && _a !== void 0 ? _a : FlightPathUtils.getShortestTurnDirection(state.currentCourse, course)) !== null && _b !== void 0 ? _b : 'right';
                    vectorIndex += this.circleVectorBuilder.buildTurnToCourse(vectors, vectorIndex, state.currentPosition, state.desiredTurnRadius.asUnit(UnitType.METER), turnDirection, state.currentCourse, course, exports.FlightPathVectorFlags.TurnToCourse, heading, isHeadingTrue);
                    if (vectorIndex > 0) {
                        const lastVector = vectors[vectorIndex - 1];
                        state.currentPosition.set(lastVector.endLat, lastVector.endLon);
                    }
                }
            }
            vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, state.currentPosition, course, distance, exports.FlightPathVectorFlags.ConstantHeading, heading, isHeadingTrue);
            vectors.length = vectorIndex;
            // NOTE: there is guaranteed to be at least one vector that was built.
            const lastVector = vectors[vectorIndex - 1];
            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            // There is a discontinuity at the end of VM legs.
            state.isDiscontinuity = true;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for hold legs.
     */
    class HoldLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of HoldLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, true);
            this.vec3Cache = ArrayUtils.create(5, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(5, () => new GeoCircle(Vec3Math.create(), 0));
            this.intersectionCache = [Vec3Math.create(), Vec3Math.create()];
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.joinGreatCircleToPointVectorBuilder = new JoinGreatCircleToPointVectorBuilder();
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[0], leg.leg.fixIcao);
            leg.calculated.courseMagVar = terminatorPos === undefined ? 0 : this.getLegMagVar(leg.leg, terminatorPos);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            var _a, _b, _c;
            const leg = legs[calculateIndex];
            const calcs = leg.calculated;
            const vectors = calcs.flightPath;
            const ingress = calcs.ingress;
            const holdPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[0], leg.leg.fixIcao);
            if (!holdPos) {
                vectors.length = 0;
                ingress.length = 0;
                calcs.ingressJoinIndex = -1;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0, ingressVectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const oppositeCourse = MathUtils.normalizeAngleDeg(course + 180);
            const inboundPath = this.geoCircleCache[0].setAsGreatCircle(holdPos, course);
            if (state.isDiscontinuity
                && options.calculateDiscontinuityVectors
                && state.currentPosition.isValid()) {
                // If we are starting in a discontinuity and we are configured to calculate discontinuity vectors, then we will
                // build a path to the hold position and insert it into the ingress vectors.
                if (!state.currentPosition.equals(holdPos)) {
                    const startPath = state.currentCourse !== undefined
                        ? this.geoCircleCache[1].setAsGreatCircle(state.currentPosition, state.currentCourse)
                        : undefined;
                    ingressVectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(ingress, ingressVectorIndex, state.currentPosition, startPath, holdPos, inboundPath, state.desiredHoldTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Discontinuity, true);
                }
                state.currentPosition.set(holdPos);
                state.currentCourse = course;
            }
            else {
                if (state.isDiscontinuity || !state.currentPosition.isValid()) {
                    // We are starting in a discontinuity or the current flight path position is not defined. We will set the
                    // current position to the hold fix and the current course to the hold (inbound) course.
                    state.currentPosition.set(holdPos);
                    state.currentCourse = course;
                }
                else {
                    if (!state.currentPosition.equals(holdPos)) {
                        // We are not starting in a discontinuity and the current flight path position is not at the hold fix. We
                        // will build a great-circle path from the current position to the hold fix and insert it into the ingress
                        // vectors.
                        ingressVectorIndex += this.circleVectorBuilder.buildGreatCircle(ingress, ingressVectorIndex, state.currentPosition, holdPos, state.currentCourse);
                        if (ingressVectorIndex > 0) {
                            state.currentCourse = FlightPathUtils.getVectorFinalCourse(ingress[ingressVectorIndex - 1]);
                        }
                        else {
                            state.currentCourse = undefined;
                        }
                    }
                    state.currentPosition.set(holdPos);
                }
            }
            // If the current flight path course is not defined, then set it to the hold (inbound) course.
            (_a = state.currentCourse) !== null && _a !== void 0 ? _a : (state.currentCourse = course);
            let distanceRad = leg.leg.distanceMinutes
                ? UnitType.NMILE.convertTo(leg.leg.distance * (state.planeSpeed.asUnit(UnitType.KNOT) / 60), UnitType.GA_RADIAN)
                : UnitType.METER.convertTo(leg.leg.distance, UnitType.GA_RADIAN);
            const turnDirection = leg.leg.turnDirection === exports.LegTurnDirection.Right ? 'right' : 'left';
            const turnDirectionSign = turnDirection === 'left' ? -1 : 1;
            const turnRadiusMeters = Math.max(state.desiredHoldTurnRadius.asUnit(UnitType.METER), 100); // Enforce an arbitrary minimum turn radius.
            const turnRadiusRad = UnitType.METER.convertTo(turnRadiusMeters, UnitType.GA_RADIAN);
            // Position the turn from the inbound leg to the outbound leg such that it is tangent to the inbound path at the
            // hold position.
            const holdPosVec = holdPos.toCartesian(this.vec3Cache[0]);
            const outboundTurnCenter = this.geoCircleCache[1].setAsGreatCircle(inboundPath.center, holdPosVec)
                .offsetDistanceAlong(holdPosVec, turnRadiusRad * turnDirectionSign, this.vec3Cache[1], Math.PI);
            const outboundTurnCircle = FlightPathUtils.getTurnCircle(outboundTurnCenter, turnRadiusRad, turnDirection, this.geoCircleCache[1]);
            let inboundStartVec;
            let inboundTurnCircle;
            let outboundPath;
            let outboundStartVec;
            let outboundEndVec;
            let needCalcCoincidentTurns = true;
            if (MathUtils.angularDistance(distanceRad, 0, 0) > GeoMath.ANGULAR_TOLERANCE) {
                // If the leg distance is not a multiple of 2 * pi great-arc radians, then the turn from the outbound leg to
                // the inbound leg (inbound turn) and the turn from the inbound leg to the outbound leg (outbound turn) do not
                // lie on the same circle. In this case, we calculate the start of the inbound leg based on the leg distance,
                // and we position the inbound turn such that it is tangent to the inbound path at the start of the inbound
                // leg. Next, we calculate the outbound path by finding the great-circle path that is tangent to both the
                // inbound and outbound turns and is not the inbound path.
                inboundStartVec = inboundPath.offsetDistanceAlong(holdPosVec, -distanceRad, this.vec3Cache[2], Math.PI);
                const inboundTurnCenter = this.geoCircleCache[2].setAsGreatCircle(inboundPath.center, inboundStartVec)
                    .offsetDistanceAlong(inboundStartVec, turnRadiusRad * turnDirectionSign, this.vec3Cache[3], Math.PI);
                inboundTurnCircle = FlightPathUtils.getTurnCircle(inboundTurnCenter, turnRadiusRad, turnDirection, this.geoCircleCache[2]);
                const outboundPathCandidates = this.intersectionCache;
                const candidateCount = this.geoCircleCache[3].set(outboundTurnCircle.center, Math.abs(MathUtils.HALF_PI - turnRadiusRad))
                    .intersection(this.geoCircleCache[4].set(inboundTurnCircle.center, Math.abs(MathUtils.HALF_PI - turnRadiusRad)), outboundPathCandidates);
                // There should always be two candidates based on how we have constructed the circles. If there aren't, then we
                // will fall back to a zero-distance hold.
                if (candidateCount === 2) {
                    outboundPath = this.geoCircleCache[3].set(outboundPathCandidates[0], MathUtils.HALF_PI);
                    outboundEndVec = outboundPath.closest(inboundTurnCenter, this.vec3Cache[4]);
                    outboundStartVec = outboundPath.closest(outboundTurnCenter, this.vec3Cache[3]);
                    needCalcCoincidentTurns = false;
                }
                else {
                    distanceRad = 0;
                }
            }
            if (needCalcCoincidentTurns) {
                // If we are here, then we need to handle the case where the turn from the outbound leg to the inbound leg and
                // the turn from the inbound leg to the outbound leg lie on the same circle. This happens when the leg distance
                // is a multiple of 2 * pi great-arc radians.
                inboundStartVec = holdPosVec;
                inboundTurnCircle = outboundTurnCircle;
                outboundStartVec = outboundTurnCircle.offsetAngleAlong(holdPosVec, Math.PI, this.vec3Cache[3], Math.PI);
                outboundEndVec = outboundStartVec;
                outboundPath = this.geoCircleCache[3].set(Vec3Math.cross(outboundStartVec, Vec3Math.cross(outboundTurnCircle.center, outboundStartVec, this.vec3Cache[2]), this.vec3Cache[2]), MathUtils.HALF_PI);
            }
            // Handle hold entry
            const normalizedEntryCourse = MathUtils.normalizeAngleDeg(state.currentCourse - course, -180); // -180 to +180
            const directionalEntryCourse = normalizedEntryCourse * turnDirectionSign;
            const isDirectEntry = directionalEntryCourse >= -70 && directionalEntryCourse <= 135;
            const skipRacetrack = leg.leg.type === exports.LegType.HF && !isDirectEntry;
            if (isDirectEntry) {
                // ---- DIRECT ENTRY ----
                if (directionalEntryCourse > 0) {
                    // The entry course is toward the outbound leg, so we just intercept the outbound leg directly, bypassing
                    // the turn from the inbound to outbound leg.
                    ingressVectorIndex += this.joinGreatCircleToPointVectorBuilder.build(ingress, ingressVectorIndex, state.currentPosition, this.geoCircleCache[4].setAsGreatCircle(state.currentPosition, state.currentCourse), outboundEndVec, outboundPath, turnDirection, turnRadiusMeters, false, true, undefined, exports.FlightPathVectorFlags.HoldDirectEntry);
                    calcs.ingressJoinIndex = 1;
                }
                else if (BitFlags.isAny((_c = (_b = ingress[0]) === null || _b === void 0 ? void 0 : _b.flags) !== null && _c !== void 0 ? _c : 0, exports.FlightPathVectorFlags.AnticipatedTurn)) {
                    // Don't erase turn anticipation for direct entries
                    ingressVectorIndex = ingress.length;
                }
            }
            else if (directionalEntryCourse > 110) {
                // ---- TEARDROP ENTRY ----
                if (directionalEntryCourse > 135) {
                    // We need to make initial turn to get to a 45-degree outbound leg.
                    const outboundCourse = course + 135 * turnDirectionSign;
                    const numTurnVectorsAdded = this.circleVectorBuilder.buildTurnToCourse(ingress, ingressVectorIndex, holdPosVec, turnRadiusMeters, turnDirection === 'left' ? 'right' : 'left', state.currentCourse, outboundCourse, exports.FlightPathVectorFlags.HoldTeardropEntry | exports.FlightPathVectorFlags.TurnToCourse);
                    if (numTurnVectorsAdded > 0) {
                        ingressVectorIndex += numTurnVectorsAdded;
                        const turnVector = ingress[ingressVectorIndex - 1];
                        state.currentPosition.set(turnVector.endLat, turnVector.endLon);
                        state.currentCourse = FlightPathUtils.getVectorFinalCourse(turnVector);
                    }
                }
                ingressVectorIndex += this.joinGreatCircleToPointVectorBuilder.build(ingress, ingressVectorIndex, state.currentPosition, this.geoCircleCache[4].setAsGreatCircle(state.currentPosition, state.currentCourse), holdPos, inboundPath, turnDirection, turnRadiusMeters, true, true, undefined, exports.FlightPathVectorFlags.HoldTeardropEntry);
                if (skipRacetrack) {
                    // If we skip the racetrack, then remove the part of the hold entry that is coincident with the inbound leg.
                    const lastEntryVector = ingress[ingressVectorIndex - 1];
                    if (lastEntryVector && FlightPathUtils.isVectorGreatCircle(lastEntryVector) && holdPos.equals(lastEntryVector.endLat, lastEntryVector.endLon)) {
                        if (UnitType.METER.convertTo(lastEntryVector.distance, UnitType.GA_RADIAN) > distanceRad + GeoMath.ANGULAR_TOLERANCE) {
                            const lastEntryVectorEnd = holdPos.offset(course + 180, distanceRad, this.geoPointCache[1]);
                            lastEntryVector.endLat = lastEntryVectorEnd.lat;
                            lastEntryVector.endLon = lastEntryVectorEnd.lon;
                            lastEntryVector.distance -= UnitType.GA_RADIAN.convertTo(distanceRad, UnitType.METER);
                        }
                        else {
                            ingressVectorIndex--;
                        }
                    }
                }
                calcs.ingressJoinIndex = 0;
            }
            else if (directionalEntryCourse < -70) {
                // ---- PARALLEL ENTRY ----
                // TODO: anticipate the turn onto the parallel course so that we don't overshoot the inbound path.
                const numTurnVectorsAdded = this.circleVectorBuilder.buildTurnToCourse(ingress, ingressVectorIndex, holdPosVec, turnRadiusMeters, turnDirection === 'left' ? 'right' : 'left', state.currentCourse, oppositeCourse, exports.FlightPathVectorFlags.HoldParallelEntry | exports.FlightPathVectorFlags.TurnToCourse);
                if (numTurnVectorsAdded > 0) {
                    ingressVectorIndex += numTurnVectorsAdded;
                    const turnVector = ingress[ingressVectorIndex - 1];
                    state.currentPosition.set(turnVector.endLat, turnVector.endLon);
                    state.currentCourse = FlightPathUtils.getVectorFinalCourse(turnVector);
                }
                ingressVectorIndex += this.procTurnVectorBuilder.build(ingress, ingressVectorIndex, state.currentPosition, this.geoCircleCache[4].setAsGreatCircle(state.currentPosition, state.currentCourse), holdPos, inboundPath, course + 135 * turnDirectionSign, turnRadiusMeters, turnDirection === 'left' ? 'right' : 'left', state.currentCourse, course, exports.FlightPathVectorFlags.HoldParallelEntry);
                calcs.ingressJoinIndex = 0;
            }
            ingress.length = ingressVectorIndex;
            if (ingress.length === 0) {
                calcs.ingressJoinIndex = -1;
            }
            if (!skipRacetrack) {
                // Turn from inbound course to outbound course.
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, outboundTurnCircle, holdPosVec, outboundStartVec, exports.FlightPathVectorFlags.TurnToCourse);
                // Outbound leg.
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, outboundPath, outboundStartVec, outboundEndVec, exports.FlightPathVectorFlags.HoldOutboundLeg);
                // Turn from outbound course to inbound course.
                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, inboundTurnCircle, outboundEndVec, inboundStartVec, exports.FlightPathVectorFlags.TurnToCourse);
            }
            // Inbound leg.
            vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, inboundPath, inboundStartVec, holdPosVec, exports.FlightPathVectorFlags.HoldInboundLeg);
            vectors.length = vectorIndex;
            state.currentPosition.set(holdPos);
            state.currentCourse = course;
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for legs that intercept the next flight plan leg.
     */
    class LegInterceptLegCalculator extends CircleInterceptLegCalculator {
        /**
         * Creates a new instance of LegInterceptLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         * @param isHeadingLeg Whether the calculator calculates flight plan legs flown with constant heading.
         */
        constructor(facilityCache, isHeadingLeg) {
            super(facilityCache, isHeadingLeg);
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined && calculateIndex + 1 < legs.length) {
                const nextLeg = legs[calculateIndex + 1];
                switch (nextLeg.leg.type) {
                    case exports.LegType.AF:
                    case exports.LegType.CF:
                    case exports.LegType.RF: {
                        const terminatorPos = this.getTerminatorPosition(nextLeg.leg, this.geoPointCache[0], nextLeg.leg.fixIcao);
                        if (terminatorPos) {
                            magVar = this.getLegMagVar(nextLeg.leg, terminatorPos);
                        }
                        break;
                    }
                    case exports.LegType.FC:
                    case exports.LegType.FM: {
                        const originPos = this.getPositionFromIcao(nextLeg.leg.fixIcao, this.geoPointCache[0]);
                        if (originPos) {
                            magVar = this.getLegMagVar(nextLeg.leg, originPos);
                        }
                        break;
                    }
                }
            }
            if (magVar === undefined) {
                let position;
                if (this.isHeadingLeg && calculateIndex === activeLegIndex && state.planePosition.isValid()) {
                    position = state.planePosition;
                }
                else if (!state.isDiscontinuity && state.currentPosition.isValid()) {
                    position = state.currentPosition;
                }
                if (position) {
                    magVar = MagVar.get(position);
                }
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        getPathToInterceptInfo(legs, calculateIndex, activeLegIndex, state, out) {
            var _a, _b;
            const nextLeg = legs[calculateIndex + 1];
            if (!nextLeg) {
                return undefined;
            }
            switch (nextLeg.leg.type) {
                case exports.LegType.AF: {
                    const center = this.getPositionFromIcao(nextLeg.leg.originIcao, this.geoPointCache[0]);
                    const end = this.getTerminatorPosition(nextLeg.leg, this.geoPointCache[1], nextLeg.leg.fixIcao);
                    if (center && end && UnitType.METER.convertTo(nextLeg.leg.rho, UnitType.GA_RADIAN) > GeoMath.ANGULAR_TOLERANCE) {
                        FlightPathUtils.getTurnCircle(center, UnitType.METER.convertTo(nextLeg.leg.rho, UnitType.GA_RADIAN), (_a = FlightPathUtils.getLegDesiredTurnDirection(nextLeg.leg)) !== null && _a !== void 0 ? _a : 'right', out.circle);
                        Vec3Math.set(NaN, NaN, NaN, out.start);
                        out.circle.closest(end.toCartesian(out.end), out.end);
                        return out;
                    }
                    break;
                }
                case exports.LegType.CF: {
                    const terminatorPos = this.getTerminatorPosition(nextLeg.leg, this.geoPointCache[0], nextLeg.leg.fixIcao);
                    const course = this.getLegTrueCourse(nextLeg.leg);
                    if (terminatorPos && course !== undefined) {
                        out.circle.setAsGreatCircle(terminatorPos, course);
                        Vec3Math.set(NaN, NaN, NaN, out.start);
                        terminatorPos.toCartesian(out.end);
                        return out;
                    }
                    break;
                }
                case exports.LegType.FC: {
                    const originPos = this.getPositionFromIcao(nextLeg.leg.fixIcao, this.geoPointCache[0]);
                    const course = this.getLegTrueCourse(nextLeg.leg);
                    if (originPos && course !== undefined && UnitType.METER.convertTo(nextLeg.leg.distance, UnitType.GA_RADIAN) > GeoMath.ANGULAR_TOLERANCE) {
                        out.circle.setAsGreatCircle(originPos, course);
                        originPos.toCartesian(out.start);
                        out.circle.offsetDistanceAlong(out.start, UnitType.METER.convertTo(nextLeg.leg.distance, UnitType.GA_RADIAN), out.end, Math.PI);
                        return out;
                    }
                    break;
                }
                case exports.LegType.FM: {
                    const originPos = this.getPositionFromIcao(nextLeg.leg.fixIcao, this.geoPointCache[0]);
                    const course = this.getLegTrueCourse(nextLeg.leg);
                    if (originPos && course !== undefined) {
                        out.circle.setAsGreatCircle(originPos, course);
                        originPos.toCartesian(out.start);
                        Vec3Math.set(NaN, NaN, NaN, out.end);
                        return out;
                    }
                    break;
                }
                case exports.LegType.RF: {
                    const center = this.getPositionFromIcao(nextLeg.leg.arcCenterFixIcao, this.geoPointCache[0]);
                    const end = this.getTerminatorPosition(nextLeg.leg, this.geoPointCache[1], nextLeg.leg.fixIcao);
                    if (center && end) {
                        const radius = center.distance(end);
                        if (radius > GeoMath.ANGULAR_TOLERANCE) {
                            FlightPathUtils.getTurnCircle(center, radius, (_b = FlightPathUtils.getLegDesiredTurnDirection(nextLeg.leg)) !== null && _b !== void 0 ? _b : 'right', out.circle);
                            Vec3Math.set(NaN, NaN, NaN, out.start);
                            end.toCartesian(out.end);
                            return out;
                        }
                    }
                    break;
                }
            }
            return undefined;
        }
    }

    /**
     * Calculates flight path vectors for legs with no path.
     */
    class NoPathLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of NoPathLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            legs[calculateIndex].calculated.courseMagVar = 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state) {
            legs[calculateIndex].calculated.flightPath.length = 0;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for procedure turn legs.
     */
    class ProcedureTurnLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of ProcedureTurnLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, true);
            this.geoPointCache = ArrayUtils.create(3, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            var _a;
            const leg = legs[calculateIndex];
            leg.calculated.courseMagVar = (_a = this.getMagVarFromIcao(leg.leg.originIcao)) !== null && _a !== void 0 ? _a : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            if (!originPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const nextLeg = legs[calculateIndex + 1];
            if (!nextLeg) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const nextLegTerminatorPos = this.getTerminatorPosition(nextLeg.leg, this.geoPointCache[1]);
            const inboundCourse = this.predictLegFinalTrueCourse(nextLeg.leg);
            if (!nextLegTerminatorPos || inboundCourse === undefined) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            const outboundCourse = MathUtils.normalizeAngleDeg(inboundCourse + 180);
            const turnInitialCourse = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            // If the initial turn does not deviate enough from the outbound course, then we can't calculate a proper procedure
            // turn.
            if (MathUtils.angularDistanceDeg(outboundCourse, turnInitialCourse, 0) < 5) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const outboundPath = this.geoCircleCache[0].setAsGreatCircle(originPos, outboundCourse);
            if ((state.isDiscontinuity && !options.calculateDiscontinuityVectors) || !state.currentPosition.isValid()) {
                // We are starting in a discontinuity or the current flight path position is not defined. We will set the
                // current position to the origin fix and the current course to the outbound course.
                state.currentPosition.set(originPos);
                state.currentCourse = outboundCourse;
            }
            else {
                // We need to build an initial path to the leg origin if the current flight path position is not at the origin.
                const needBuildInitialPath = !state.currentPosition.equals(originPos);
                if (needBuildInitialPath
                    || (state.isDiscontinuity && options.calculateDiscontinuityVectors)) {
                    if (needBuildInitialPath) {
                        const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None)
                            | (state.isFallback && state.currentCourse !== undefined ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None);
                        const startPath = state.currentCourse !== undefined
                            ? this.geoCircleCache[1].setAsGreatCircle(state.currentPosition, state.currentCourse)
                            : undefined;
                        vectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, startPath, originPos, outboundPath, state.desiredHoldTurnRadius.asUnit(UnitType.METER), undefined, flags, true);
                    }
                    state.currentPosition.set(originPos);
                    state.currentCourse = outboundCourse;
                }
            }
            // We must intercept the next leg at least 1 nautical mile from the terminator position.
            const inboundPathEndpoint = nextLegTerminatorPos.offset(outboundCourse, UnitType.NMILE.convertTo(1, UnitType.GA_RADIAN));
            const inboundPath = this.geoCircleCache[1].setAsGreatCircle(inboundPathEndpoint, inboundCourse);
            const desiredTurnDirection = FlightPathUtils.getLegDesiredTurnDirection(leg.leg);
            vectorIndex += this.procTurnVectorBuilder.build(vectors, vectorIndex, originPos, outboundPath, inboundPathEndpoint, inboundPath, turnInitialCourse, state.desiredCourseReversalTurnRadius.asUnit(UnitType.METER), desiredTurnDirection, outboundCourse, inboundCourse);
            vectors.length = vectorIndex;
            // The procedure turn vector builder is guaranteed to add at least one vector.
            const lastVector = vectors[vectorIndex - 1];
            state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
        /**
         * Predicts the final true course of a leg at its terminator fix, in degrees.
         * @param leg A flight plan leg.
         * @returns The predicted final true course of a leg at its terminator fix, in degrees, or `undefined` if the course
         * could not be determined.
         */
        predictLegFinalTrueCourse(leg) {
            switch (leg.type) {
                case exports.LegType.IF:
                case exports.LegType.CF:
                    return this.getLegTrueCourse(leg);
                default:
                    return undefined;
            }
        }
    }

    /**
     * Calculates flight path vectors for legs that intercept a radial from a reference fix.
     */
    class RadialInterceptLegCalculator extends CircleInterceptLegCalculator {
        /**
         * Creates a new instance of RadialInterceptLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         * @param isHeadingLeg Whether the calculator calculates flight plan legs flown with constant heading.
         */
        constructor(facilityCache, isHeadingLeg) {
            super(facilityCache, isHeadingLeg);
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex, activeLegIndex, state) {
            const leg = legs[calculateIndex];
            let magVar = this.getMagVarFromIcao(leg.leg.originIcao);
            if (magVar === undefined) {
                let position;
                if (this.isHeadingLeg && calculateIndex === activeLegIndex && state.planePosition.isValid()) {
                    position = state.planePosition;
                }
                else if (!state.isDiscontinuity && state.currentPosition.isValid()) {
                    position = state.currentPosition;
                }
                if (position) {
                    magVar = MagVar.get(position);
                }
            }
            leg.calculated.courseMagVar = magVar !== null && magVar !== void 0 ? magVar : 0;
        }
        /** @inheritDoc */
        getPathToInterceptInfo(legs, calculateIndex, activeLegIndex, state, out) {
            const leg = legs[calculateIndex];
            const radialCenter = this.getPositionFromIcao(leg.leg.originIcao, this.geoPointCache[0]);
            if (radialCenter) {
                out.circle.setAsGreatCircle(radialCenter, MagVar.magneticToTrue(leg.leg.theta, leg.calculated.courseMagVar));
                radialCenter.toCartesian(out.start);
                Vec3Math.set(NaN, NaN, NaN, out.end);
                return out;
            }
            else {
                return undefined;
            }
        }
    }

    /**
     * Calculates flight path vectors for track from fix legs.
     */
    class TrackFromFixLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of TrackFromFixLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            const leg = legs[calculateIndex];
            const startFacility = this.facilityCache.get(leg.leg.fixIcao);
            leg.calculated.courseMagVar = startFacility === undefined ? 0 : this.getLegMagVar(leg.leg, startFacility);
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const originPos = this.getPositionFromIcao(leg.leg.fixIcao, this.geoPointCache[0]);
            if (!originPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            const course = leg.leg.trueDegrees ? leg.leg.course : MagVar.magneticToTrue(leg.leg.course, leg.calculated.courseMagVar);
            const endPos = originPos.offset(course, UnitType.METER.convertTo(leg.leg.distance, UnitType.GA_RADIAN), this.geoPointCache[1]);
            if ((!state.isDiscontinuity || options.calculateDiscontinuityVectors)
                && state.isFallback && state.currentPosition.isValid() && state.currentCourse !== undefined) {
                // If we are starting in a fallback state and the current position is defined, then path a direct course to the
                // leg end point.
                const flags = exports.FlightPathVectorFlags.Fallback
                    | (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None);
                vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, state.currentCourse, endPos, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, flags);
            }
            else {
                let startPos;
                if (!state.currentPosition.isValid()) {
                    // If the current flight path position is not valid, then we will start at the leg origin.
                    startPos = originPos;
                }
                else if (state.isDiscontinuity) {
                    // If we are starting in a discontinuity, then we will start at the leg origin. If we are configured to
                    // calculate discontinuity vectors, then we will also build a path to the origin point.
                    if (options.calculateDiscontinuityVectors) {
                        const startPath = state.currentCourse
                            ? this.geoCircleCache[0].setAsGreatCircle(state.currentPosition, state.currentCourse)
                            : undefined;
                        const endPath = this.geoCircleCache[1].setAsGreatCircle(originPos, course);
                        vectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, startPath, originPos, endPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, exports.FlightPathVectorFlags.Discontinuity, true);
                    }
                    startPos = originPos;
                }
                else {
                    startPos = state.currentPosition;
                }
                // Create a great-circle path from the start point to the end point.
                if (startPos.distance(endPos) > GeoMath.ANGULAR_TOLERANCE) {
                    vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, startPos, endPos, course);
                }
            }
            vectors.length = vectorIndex;
            const hasInvalidStart = state.isDiscontinuity || !state.currentPosition.isValid();
            state.currentPosition.set(endPos);
            if (hasInvalidStart) {
                state.currentCourse = undefined;
            }
            else if (vectorIndex > 0) {
                const lastVector = vectors[vectorIndex - 1];
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(lastVector);
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for track to fix legs.
     */
    class TrackToFixLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of TrackToFixLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.geoPointCache = ArrayUtils.create(1, () => new GeoPoint(0, 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.directToPointVectorBuilder = new DirectToPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            var _a;
            const leg = legs[calculateIndex];
            leg.calculated.courseMagVar = (_a = this.getTerminatorMagVar(leg.leg)) !== null && _a !== void 0 ? _a : 0;
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[0], leg.leg.fixIcao);
            if (!terminatorPos) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            // Only build vectors if...
            if (
            // ... the current flight path position is defined...
            state.currentPosition.isValid()
                // ... and the leg is of non-zero length.
                && !state.currentPosition.equals(terminatorPos)) {
                // If the leg starts in a discontinuity, then only build vectors if we are configured to calculate discontinuity
                // vectors.
                if (!state.isDiscontinuity || options.calculateDiscontinuityVectors) {
                    const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None);
                    if ((state.isDiscontinuity || state.isFallback) && state.currentCourse !== undefined) {
                        vectorIndex += this.directToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, state.currentCourse, terminatorPos, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, flags | (state.isFallback ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None));
                    }
                    else {
                        // Build vectors for a great-circle path from the start to the leg terminator.
                        vectorIndex += this.circleVectorBuilder.buildGreatCircle(vectors, vectorIndex, state.currentPosition, terminatorPos, state.currentCourse, flags);
                    }
                }
            }
            const hasInvalidStart = state.isDiscontinuity || !state.currentPosition.isValid();
            // If the starting position is undefined or the leg begins in a discontinuity, the leg is an IF leg, and the
            // terminator fix is a runway, then set the current course to the runway course.
            if (hasInvalidStart
                && leg.leg.type === exports.LegType.IF
                && ICAO.isFacility(leg.leg.fixIcao, exports.FacilityType.RWY)) {
                const facility = this.facilityCache.get(leg.leg.fixIcao);
                if (facility && FacilityUtils.isFacilityType(facility, exports.FacilityType.RWY)) {
                    state.currentCourse = facility.runway.course;
                }
            }
            vectors.length = vectorIndex;
            state.currentPosition.set(terminatorPos);
            if (hasInvalidStart) {
                state.currentCourse = undefined;
            }
            else if (vectorIndex > 0) {
                state.currentCourse = FlightPathUtils.getVectorFinalCourse(vectors[vectorIndex - 1]);
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }

    /**
     * Calculates flight path vectors for legs that define a turn ending at a defined terminator fix.
     */
    class TurnToFixLegCalculator extends AbstractFlightPathLegCalculator {
        /**
         * Creates a new instance of TurnToFixLegCalculator.
         * @param facilityCache This calculator's cache of facilities.
         */
        constructor(facilityCache) {
            super(facilityCache, false);
            this.vec3Cache = ArrayUtils.create(2, () => Vec3Math.create());
            this.geoPointCache = ArrayUtils.create(2, () => new GeoPoint(0, 0));
            this.geoCircleCache = ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0));
            this.circleVectorBuilder = new CircleVectorBuilder();
            this.directToJoinGreatCircleToPointVectorBuilder = new DirectToJoinGreatCircleAtPointVectorBuilder();
        }
        /** @inheritDoc */
        calculateVectors(legs, calculateIndex, activeLegIndex, state, options) {
            const leg = legs[calculateIndex];
            const vectors = leg.calculated.flightPath;
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.geoPointCache[0], leg.leg.fixIcao);
            const turnCenter = this.getTurnCenter(leg.leg);
            if (!terminatorPos || !turnCenter) {
                vectors.length = 0;
                state.isFallback = false;
                return;
            }
            let vectorIndex = 0;
            // Only build vectors if...
            if (
            // ... the current flight path position is defined...
            state.currentPosition.isValid()
                // ... and the leg is of non-zero length.
                && !state.currentPosition.equals(terminatorPos)) {
                // If the leg starts in a discontinuity, then only build vectors if we are configured to calculate discontinuity
                // vectors.
                if (!state.isDiscontinuity || options.calculateDiscontinuityVectors) {
                    const direction = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 'left' : 'right';
                    if (state.isDiscontinuity || (state.isFallback && state.currentCourse !== undefined)) {
                        // If we are starting in a discontinuity or a fallback state, then the start of the leg is not guaranteed to
                        // lie anywhere near the leg's defined turn circle. Therefore, we will ignore the turn circle and build a
                        // path that ends at the leg terminator in the same direction that the turn circle would cross the
                        // terminator.
                        const endVec = terminatorPos.toCartesian(this.vec3Cache[0]);
                        const radial = this.geoCircleCache[0].setAsGreatCircle(turnCenter, endVec);
                        if (radial.isValid()) {
                            const startPath = state.currentCourse !== undefined
                                ? this.geoCircleCache[1].setAsGreatCircle(state.currentPosition, state.currentCourse)
                                : undefined;
                            const endPath = radial.rotate(endVec, direction === 'left' ? -MathUtils.HALF_PI : MathUtils.HALF_PI, Math.PI);
                            const flags = (state.isDiscontinuity ? exports.FlightPathVectorFlags.Discontinuity : exports.FlightPathVectorFlags.None)
                                | (state.isFallback && state.currentCourse !== undefined ? exports.FlightPathVectorFlags.Fallback : exports.FlightPathVectorFlags.None);
                            vectorIndex += this.directToJoinGreatCircleToPointVectorBuilder.build(vectors, vectorIndex, state.currentPosition, startPath, endVec, endPath, state.desiredTurnRadius.asUnit(UnitType.METER), undefined, flags, true);
                        }
                    }
                    else {
                        const radius = this.getTurnRadius(leg.leg, turnCenter);
                        if (radius !== undefined && radius > GeoMath.ANGULAR_TOLERANCE) {
                            const turnCircle = FlightPathUtils.getTurnCircle(turnCenter, radius, direction, this.geoCircleCache[0]);
                            const startVec = turnCircle.closest(state.currentPosition, this.vec3Cache[0]);
                            const endVec = turnCircle.closest(terminatorPos, this.vec3Cache[1]);
                            if (Vec3Math.isFinite(startVec) && Vec3Math.isFinite(endVec)) {
                                vectorIndex += this.circleVectorBuilder.build(vectors, vectorIndex, turnCircle, startVec, endVec, exports.FlightPathVectorFlags.Arc);
                                state.currentCourse = turnCircle.bearingAt(endVec);
                            }
                        }
                    }
                }
            }
            vectors.length = vectorIndex;
            const hasInvalidStart = state.isDiscontinuity || !state.currentPosition.isValid();
            if (vectorIndex > 0) {
                const lastVector = vectors[vectorIndex - 1];
                state.currentPosition.set(lastVector.endLat, lastVector.endLon);
            }
            else {
                state.currentPosition.set(terminatorPos);
            }
            if (hasInvalidStart) {
                state.currentCourse = undefined;
            }
            state.isDiscontinuity = false;
            state.isFallback = false;
        }
    }
    /**
     * Calculates flight path vectors for arc to fix legs.
     */
    class ArcToFixLegCalculator extends TurnToFixLegCalculator {
        constructor() {
            super(...arguments);
            this.afGeoPointCache = [new GeoPoint(0, 0)];
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            var _a;
            const leg = legs[calculateIndex];
            const terminatorPos = this.getTerminatorPosition(leg.leg, this.afGeoPointCache[0], leg.leg.fixIcao);
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            leg.calculated.courseMagVar = (_a = this.getLegMagVar(leg.leg, terminatorPos)) !== null && _a !== void 0 ? _a : 0;
        }
        /** @inheritDoc */
        getTurnCenter(leg) {
            return this.facilityCache.get(leg.originIcao);
        }
        /** @inheritDoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        getTurnRadius(leg, center) {
            return UnitType.METER.convertTo(leg.rho, UnitType.GA_RADIAN);
        }
    }
    /**
     * Calculates flight path vectors for radius to fix legs.
     */
    class RadiusToFixLegCalculator extends TurnToFixLegCalculator {
        constructor() {
            super(...arguments);
            this.rfGeoPointCache = [new GeoPoint(0, 0)];
        }
        /** @inheritDoc */
        calculateMagVar(legs, calculateIndex) {
            var _a;
            const leg = legs[calculateIndex];
            leg.calculated.courseMagVar = (_a = this.getTerminatorMagVar(leg.leg)) !== null && _a !== void 0 ? _a : 0;
        }
        /** @inheritDoc */
        getTurnCenter(leg) {
            return this.facilityCache.get(leg.arcCenterFixIcao);
        }
        /** @inheritDoc */
        getTurnRadius(leg, center) {
            var _a;
            return (_a = this.getPositionFromIcao(leg.fixIcao, this.rfGeoPointCache[0])) === null || _a === void 0 ? void 0 : _a.distance(center);
        }
    }

    /**
     * A sorted array.
     */
    class SortedArray {
        // eslint-disable-next-line jsdoc/require-returns
        /** A read-only version of the array object backing this sorted array. */
        get array() {
            return this._array;
        }
        /**
         * The number of elements in this array.
         * @returns The number of elements in the array.
         */
        get length() {
            return this._array.length;
        }
        /**
         * Constructor.
         * @param comparatorFunc A function which defines the relative sorting priority of two elements. The function should
         * return 0 if its arguments are to be sorted identically, a negative number if the first argument is to be sorted
         * before the second argument, and a positive number if the first argument is to be sorted after the second argument.
         * @param equalityFunc A function which checks if two elements are equal. Defaults to the strict equality comparison
         * (`===`) if not defined.
         */
        constructor(comparatorFunc, equalityFunc = SortedArray.DEFAULT_EQUALITY_FUNC) {
            this.comparatorFunc = comparatorFunc;
            this.equalityFunc = equalityFunc;
            this._array = [];
        }
        /**
         * Finds the index of the first or last element in this array whose sorting priority is equal to a query element. If
         * no such element in this array exists, `-(index + 1)` is returned, where `index` is the index at which the query
         * element would be found if it were contained in the array.
         * @param element The query element.
         * @param first Whether to find the first index.
         * @returns The index of the first or last element in this array with the same sorting priority as the query, or
         * `-(index + 1)` if no such element exists, where `index` is the index at which the query element would be found if
         * it were contained in the array.
         */
        findIndex(element, first = true) {
            let min = 0;
            let max = this._array.length;
            let index = Math.floor((min + max) / 2);
            while (min < max) {
                const compare = this.comparatorFunc(element, this._array[index]);
                if (compare < 0) {
                    max = index;
                }
                else if (compare > 0) {
                    min = index + 1;
                }
                else {
                    const delta = first ? -1 : 1;
                    while (index + delta >= 0 && index + delta < this._array.length && this.comparatorFunc(element, this._array[index + delta]) === 0) {
                        index += delta;
                    }
                    return index;
                }
                index = Math.floor((min + max) / 2);
            }
            return -(index + 1);
        }
        /**
         * Finds the index of the first element in this array which equals a query element, starting at a specified index.
         * The search proceeds toward the end of the array, ending at the first index containing an element whose sorting
         * priority does not equal the query, or the end of the array, whichever comes first. If no such element in this
         * array exists, -1 is returned instead.
         * @param element The query element.
         * @param startIndex The index at which to start the search.
         * @returns The index of the first element in this array which equals the query element, or -1 if no such element
         * exists.
         */
        searchEquals(element, startIndex) {
            let index = startIndex;
            while (index >= 0 && index < this._array.length && this.comparatorFunc(element, this._array[index]) === 0) {
                if (this.equalityFunc(element, this._array[index])) {
                    return index;
                }
                index++;
            }
            return -1;
        }
        /**
         * Gets the element at a specified index.
         * @param index An index.
         * @returns The element at the specified index.
         * @throws RangeError if index is out of bounds.
         */
        get(index) {
            if (index < 0 || index >= this._array.length) {
                throw new RangeError();
            }
            return this._array[index];
        }
        /**
         * Gets the element at a specified index, if it exists.
         * @param index An index.
         * @returns The element at the specified index, or undefined if the index is out of bounds.
         */
        peek(index) {
            return this._array[index];
        }
        /**
         * Gets the first element in this array.
         * @returns The first element in this array.
         * @throws RangeError if this array is empty.
         */
        first() {
            if (this._array.length === 0) {
                throw new RangeError();
            }
            return this._array[0];
        }
        /**
         * Gets the first element in this array, if it exists.
         * @returns The first element in this array, or undefined if this array is empty.
         */
        peekFirst() {
            return this._array[0];
        }
        /**
         * Gets the last element in this array.
         * @returns The last element in this array.
         * @throws RangeError if this array is empty.
         */
        last() {
            if (this._array.length === 0) {
                throw new RangeError();
            }
            return this._array[this._array.length - 1];
        }
        /**
         * Gets the last element in this array, if it exists.
         * @returns The last element in this array, or undefined if this array is empty.
         */
        peekLast() {
            return this._array[this._array.length - 1];
        }
        /**
         * Checks whether this array contains an element. Returns true if and only if there is at least one element in this
         * array which is equal to the specified element according to this array's equality function.
         * @param element The element to check.
         * @returns Whether this array contains the element.
         */
        has(element) {
            return this.searchEquals(element, this.findIndex(element)) >= 0;
        }
        /**
         * Inserts an element into this array. The element will be inserted at the greatest index such that it is located
         * before all the existing elements in the array sorted after it according to this array's sorting function. All
         * existing elements located at indexes greater than or equal to the index at which the element was inserted are
         * shifted to the right.
         * @param element The element to insert.
         * @returns The index at which the element was placed.
         */
        insert(element) {
            let index = this.findIndex(element, false);
            if (index < 0) {
                index = -index - 1;
            }
            this._array.splice(index, 0, element);
            return index;
        }
        /**
         * Inserts all elements in an Iterable into this array. Each element is inserted according to the same behavior used
         * by the `insert()` method. If an element appears more than once in the iterable, one instance of that element will
         * be inserted into this array for each time the element appears in the iterable.
         * @param elements An iterable of elements to insert.
         * @returns The number of elements inserted.
         */
        insertAll(elements) {
            const sorted = Array.from(elements).sort(this.comparatorFunc);
            let toInsertIndex = 0;
            let toInsert = sorted[toInsertIndex];
            const len = this._array.length;
            const insertLen = sorted.length;
            for (let i = 0; i < len && toInsertIndex < insertLen; i++) {
                if (this.comparatorFunc(toInsert, this._array[i]) > 0) {
                    this._array.splice(i, 0, toInsert);
                    toInsert = sorted[++toInsertIndex];
                }
            }
            for (; toInsertIndex < insertLen; toInsertIndex++) {
                this._array.push(sorted[toInsertIndex]);
            }
            return sorted.length;
        }
        /**
         * Removes the first occurrence of an element from this array. This array is searched for the first element which
         * is equal to the specified element according to this array's equality function, the matching element is removed,
         * and all elements after it are shifted to the left.
         * @param element The element to remove.
         * @returns The (former) index of the removed element, or -1 if no element was removed.
         */
        remove(element) {
            const index = this.searchEquals(element, this.findIndex(element));
            if (index >= 0) {
                this._array.splice(index, 1);
            }
            return index;
        }
        /**
         * Removes all elements in an Iterable from this array. Each element is removed according to the behavior used by the
         * `remove()` method. If an element appears more than once in the iterable, one instance of that element will be
         * removed from this array for each time the element appears in the iterable.
         * @param elements An iterable of elements to remove.
         * @returns The number of elements removed.
         */
        removeAll(elements) {
            const sorted = Array.from(elements).sort(this.comparatorFunc);
            let numRemoved = 0;
            let toRemoveIndex = 0;
            let toRemove = sorted[toRemoveIndex];
            const len = this._array.length;
            const removeLen = sorted.length;
            for (let i = 0; i < len && toRemoveIndex < removeLen; i++) {
                if (this.equalityFunc(toRemove, this._array[i])) {
                    this._array.splice(i--, 1);
                    toRemove = sorted[++toRemoveIndex];
                    numRemoved++;
                }
            }
            return numRemoved;
        }
        /**
         * Removes an element at a specific index from this array and returns it.
         * @param index The index of the element to remove.
         * @returns The removed element, or `undefined` if no element was removed.
         */
        removeAt(index) {
            if (index < 0 || index >= this._array.length) {
                return undefined;
            }
            return this._array.splice(index, 1)[0];
        }
        /**
         * Removes the last element from this array and returns it.
         * @returns The removed element, or `undefined` if the array was empty.
         */
        pop() {
            return this._array.pop();
        }
        /**
         * Removes the first element from this array and returns it.
         * @returns The removed element, or `undefined` if the array was empty.
         */
        shift() {
            return this._array.shift();
        }
        /**
         * Re-sorts this array using its sorting function.
         */
        resort() {
            this._array.sort(this.comparatorFunc);
        }
        /**
         * Finds the index of the first occurrence of an element in this array. This array is searched for the first element
         * which is equal to the specified element according to this array's equality function, and its index is returned.
         * @param element The element for which to search.
         * @returns The index of the first occurrence of the specified element, or -1 if no such element was found.
         */
        indexOf(element) {
            return this.searchEquals(element, this.findIndex(element));
        }
        /**
         * Searches this array for the first element whose sorting priority is equal to a query element. If no such element
         * is found, then undefined is returned instead.
         * @param query The query element.
         * @returns The first element in the array with the same sorting priority as the query, or undefined if no such
         * element exists.
         */
        match(query) {
            const index = this.matchIndex(query);
            return this._array[index];
        }
        /**
         * Searches this array for the index of the first element whose sorting priority is equal to a query element. If no
         * such element is found, then `-(index + 1)` is returned instead, where `index` is the index at which the query
         * element would be found if it were contained in the array.
         * @param query The query element.
         * @returns The index of the first element in this array with the same sorting priority as the query, or
         * `-(index + 1)` if no such element exists, where `index` is the index at which the query element would be found if
         * it were contained in the array.
         */
        matchIndex(query) {
            return this.findIndex(query);
        }
        /**
         * Removes all elements from this array.
         */
        clear() {
            this._array.length = 0;
        }
        /**
         * Gets an IterableIterator over all elements in this array.
         * @returns An IterableIterator over all elements in this array.
         */
        values() {
            return this._array.values();
        }
        /** @inheritdoc */
        [Symbol.iterator]() {
            return this._array.values();
        }
    }
    SortedArray.DEFAULT_EQUALITY_FUNC = (a, b) => a === b;

    /**
     * A linearly interpolated N-dimensional lookup table.
     */
    class LerpLookupTable {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of dimensions in this table. */
        get dimensionCount() {
            return this._dimensionCount;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(arg) {
            this.table = new SortedArray(LerpLookupTable.BREAKPOINT_COMPARATOR);
            if (typeof arg === 'number') {
                this._dimensionCount = isFinite(arg) ? Math.max(0, arg) : 0;
                return;
            }
            const leastDimension = arg.reduce((accum, current) => (current.length < accum.length) ? current : accum);
            this._dimensionCount = Math.max(0, leastDimension ? (leastDimension.length - 1) : 0);
            if (this._dimensionCount === 0) {
                return;
            }
            for (let i = 0; i < arg.length; i++) {
                this.insertBreakpoint(arg[i]);
            }
        }
        /**
         * Inserts a breakpoint into this table. If the breakpoint has more dimensions than this table, only the first `N`
         * keys of the breakpoint will be used, where `N` is the dimension count of this table.
         * @param breakpoint A breakpoint, as a number array with the value at index 0 followed by the keys for each
         * dimension.
         * @returns This table, after the breakpoint has been inserted.
         * @throws Error if this table has zero dimensions, or the breakpoint has fewer dimensions than this table.
         */
        insertBreakpoint(breakpoint) {
            if (this._dimensionCount === 0) {
                throw new Error('LerpLookupTable: cannot insert a breakpoint into a 0-dimensional table');
            }
            if (breakpoint.length - 1 < this._dimensionCount) {
                throw new Error(`LerpLookupTable: cannot insert a ${breakpoint.length - 1}-dimensional breakpoint into a ${this._dimensionCount}-dimensional table`);
            }
            this.insertBreakpointHelper(breakpoint, 0, this.table);
            return this;
        }
        /**
         * Helper method for inserting a breakpoint into this table.
         * @param breakpoint The breakpoint to insert.
         * @param dimension The current dimension being evaluated.
         * @param array The array of dimensional breakpoints into which the breakpoint should be inserted.
         */
        insertBreakpointHelper(breakpoint, dimension, array) {
            const dimensionKey = breakpoint[dimension + 1];
            const query = LerpLookupTable.tempBreakpoint;
            query.key = dimensionKey;
            if (dimension === this._dimensionCount - 1) {
                let match = array.match(query);
                if (!match) {
                    match = { key: dimensionKey, value: breakpoint[0] };
                    array.insert(match);
                }
            }
            else {
                let next = array.match(query);
                if (!next) {
                    array.insert(next = { key: dimensionKey, array: new SortedArray(LerpLookupTable.BREAKPOINT_COMPARATOR) });
                }
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.insertBreakpointHelper(breakpoint, dimension + 1, next.array);
            }
        }
        /**
         * Looks up a value in this table using a specified key. The returned value will be linearly interpolated from
         * surrounding breakpoints if the key is not an exact match for any of the table's breakpoints.
         * @param key The lookup key, as an ordered N-tuple of numbers.
         * @returns The value corresponding to the specified key.
         * @throws Error if this table has zero dimensions, the key has fewer dimensions than this table, or a value could
         * not be retrieved.
         */
        get(...key) {
            if (this._dimensionCount === 0) {
                throw new Error('LerpLookupTable: cannot look up a key in a 0-dimensional table');
            }
            if (key.length < this._dimensionCount) {
                throw new Error(`LerpLookupTable: cannot look up a ${key.length}-dimensional key in a ${this._dimensionCount}-dimensional table`);
            }
            const value = this.lookupHelper(key, 0, this.table);
            if (value === undefined) {
                throw new Error(`LerpLookupTable: could not retrieve value for key ${key}`);
            }
            return value;
        }
        /**
         * Helper method for looking up a key in this table.
         * @param key The key to look up.
         * @param dimension The current dimension being evaluated.
         * @param lookupArray The array containing breakpoints in the next lower dimension in which to search for the key.
         * @returns The interpolated value of the key at the specified dimension.
         */
        lookupHelper(key, dimension, lookupArray) {
            const dimensionKey = key[dimension];
            const query = LerpLookupTable.tempBreakpoint;
            query.key = dimensionKey;
            const index = lookupArray.matchIndex(query);
            let start;
            let end;
            if (index >= 0) {
                start = lookupArray.peek(index);
                end = start;
            }
            else {
                start = lookupArray.peek(-index - 2);
                end = lookupArray.peek(-index - 1);
                if (!start) {
                    start = end;
                }
                if (!end) {
                    end = start;
                }
            }
            if (!start || !end) {
                return undefined;
            }
            let startValue;
            let endValue;
            if (dimension === this.dimensionCount - 1) {
                startValue = start.value;
                endValue = end.value;
            }
            else {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                startValue = this.lookupHelper(key, dimension + 1, start.array);
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                endValue = this.lookupHelper(key, dimension + 1, end.array);
            }
            if (startValue === undefined || endValue === undefined) {
                return undefined;
            }
            if (startValue === endValue) {
                return startValue;
            }
            return MathUtils.lerp(dimensionKey, start.key, end.key, startValue, endValue);
        }
    }
    LerpLookupTable.BREAKPOINT_COMPARATOR = (a, b) => a.key - b.key;
    LerpLookupTable.tempBreakpoint = { key: 0 };

    /**
     * Utility class for working with flight plans.
     */
    class FlightPlanUtils {
        /**
         * Checks if a leg type is an "to altitude" leg type.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a "to altitude" leg type.
         */
        static isAltitudeLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.ALTITUDE_LEG_TYPES, legType);
        }
        /**
         * Checks if a leg type is a "heading to" leg type.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a "heading to" leg type.
         */
        static isHeadingToLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.HEADING_LEG_TYPES, legType);
        }
        /**
         * Checks if a leg type is a "to radial" leg type.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a "to radial" leg type.
         */
        static isToRadialLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.TO_RADIAL_LEG_TYPES, legType);
        }
        /**
         * Checks if a leg type is a "hold" leg type.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a "hold" leg type.
         */
        static isHoldLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.HOLD_LEG_TYPES, legType);
        }
        /**
         * Checks if a leg type is a manual termination leg type that ends in a discontinuity.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a manual termination leg type that ends in a discontinuity.
         */
        static isManualDiscontinuityLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.MANUAL_DISCO_LEG_TYPES, legType);
        }
        /**
         * Checks if a leg type is a discontinuity leg type.
         * @param legType The leg type to check.
         * @returns Whether the leg type is a discontinuity leg type.
         */
        static isDiscontinuityLeg(legType) {
            return ArrayUtils.includes(FlightPlanUtils.DISCO_LEG_TYPES, legType);
        }
        /**
         * Gets the ICAO of the facility defining the terminator of a flight plan leg.
         * @param leg A flight plan leg.
         * @returns The ICAO of the facility defining the terminator of the specified flight plan leg, or `undefined` if
         * the leg's terminator is not defined by a facility.
         */
        static getTerminatorIcao(leg) {
            switch (leg.type) {
                case exports.LegType.IF:
                case exports.LegType.TF:
                case exports.LegType.DF:
                case exports.LegType.CF:
                case exports.LegType.AF:
                case exports.LegType.RF:
                case exports.LegType.HA:
                case exports.LegType.HF:
                case exports.LegType.HM:
                    return leg.fixIcao;
                default:
                    return undefined;
            }
        }
        /**
         * Creates a new {@link FlightPlanLegIndexes} object with all indexes set to `-1`.
         * @returns A new `FlightPlanLegIndexes` object with all indexes set to `-1`.
         */
        static emptyLegIndexes() {
            return { segmentIndex: -1, segmentLegIndex: -1 };
        }
        /**
         * Converts all runway ICAO references in a flight plan leg to the runway ICAO format used by the MSFS avionics
         * SDK.
         * @param leg The flight plan leg to change.
         * @returns The specified flight plan leg, after all of its runway ICAO references have been changed to the format
         * used by the MSFS avionics SDK.
         */
        static convertLegRunwayIcaosToSdkFormat(leg) {
            if (ICAO.isValueFacility(leg.fixIcaoStruct, exports.FacilityType.RWY)) {
                const convertedIcao = ICAO.value(leg.fixIcaoStruct.type, '', leg.fixIcaoStruct.airport, leg.fixIcaoStruct.ident);
                leg.fixIcaoStruct = convertedIcao;
                leg.fixIcao = ICAO.tryValueToStringV1(convertedIcao);
            }
            if (ICAO.isValueFacility(leg.originIcaoStruct, exports.FacilityType.RWY)) {
                const convertedIcao = ICAO.value(leg.originIcaoStruct.type, '', leg.originIcaoStruct.airport, leg.originIcaoStruct.ident);
                leg.originIcao = ICAO.tryValueToStringV1(convertedIcao);
            }
            if (ICAO.isValueFacility(leg.arcCenterFixIcaoStruct, exports.FacilityType.RWY)) {
                const convertedIcao = ICAO.value(leg.arcCenterFixIcaoStruct.type, '', leg.arcCenterFixIcaoStruct.airport, leg.arcCenterFixIcaoStruct.ident);
                leg.arcCenterFixIcao = ICAO.tryValueToStringV1(convertedIcao);
            }
            return leg;
        }
        /**
         * Sets an ICAO property on a {@link FlightPlanLeg}, ensuring that both the associated value and string (V1)
         * properties stay in sync.
         * @param leg The flight plan leg to change.
         * @param prop The property to set.
         * @param icao The ICAO value to set on the property.
         * @returns The specified flight plan leg, after it has been changed.
         */
        static setLegIcao(leg, prop, icao) {
            switch (prop) {
                case 'fixIcaoStruct':
                    leg.fixIcaoStruct = icao;
                    leg.fixIcao = ICAO.valueToStringV1(icao);
                    break;
                case 'originIcaoStruct':
                    leg.originIcaoStruct = icao;
                    leg.originIcao = ICAO.valueToStringV1(icao);
                    break;
                case 'arcCenterFixIcaoStruct':
                    leg.arcCenterFixIcaoStruct = icao;
                    leg.arcCenterFixIcao = ICAO.valueToStringV1(icao);
                    break;
            }
            return leg;
        }
    }
    /** Array of "to altitude" leg types. */
    FlightPlanUtils.ALTITUDE_LEG_TYPES = [exports.LegType.CA, exports.LegType.FA, exports.LegType.VA];
    /** Array of "heading to" leg types. */
    FlightPlanUtils.HEADING_LEG_TYPES = [exports.LegType.VA, exports.LegType.VD, exports.LegType.VI, exports.LegType.VM, exports.LegType.VR];
    /** Array of "to radial" leg types. */
    FlightPlanUtils.TO_RADIAL_LEG_TYPES = [exports.LegType.CR, exports.LegType.VR];
    /** Array of "hold" leg types. */
    FlightPlanUtils.HOLD_LEG_TYPES = [exports.LegType.HA, exports.LegType.HF, exports.LegType.HM];
    /** Array of manual termination leg types that end in a discontinuity. */
    FlightPlanUtils.MANUAL_DISCO_LEG_TYPES = [exports.LegType.FM, exports.LegType.VM];
    /** Array of discontinuity leg types. */
    FlightPlanUtils.DISCO_LEG_TYPES = [exports.LegType.Discontinuity, exports.LegType.ThruDiscontinuity];

    /**
     * A calculator of lateral flight paths for transitions between adjacent flight plan legs.
     */
    class FlightPathLegToLegCalculator {
        constructor() {
            this.procTurnVectorBuilder = new ProcedureTurnVectorBuilder();
            this.setAnticipatedTurnCache = {
                geoCircle: ArrayUtils.create(1, () => new GeoCircle(Vec3Math.create(), 0))
            };
            this.trackTrackCache = {
                vec3: ArrayUtils.create(3, () => Vec3Math.create()),
                geoPoint: ArrayUtils.create(4, () => new GeoPoint(0, 0)),
                geoCircle: ArrayUtils.create(2, () => new GeoCircle(Vec3Math.create(), 0))
            };
            this.trackTurnCache = {
                vec3: ArrayUtils.create(10, () => Vec3Math.create()),
                geoPoint: ArrayUtils.create(2, () => new GeoPoint(0, 0)),
                geoCircle: ArrayUtils.create(5, () => new GeoCircle(Vec3Math.create(), 0)),
                intersection: ArrayUtils.create(1, () => [Vec3Math.create(), Vec3Math.create()]),
            };
            this.trackTurnIntersectionValidCache = {
                vec3: ArrayUtils.create(2, () => Vec3Math.create())
            };
        }
        /**
         * Calculates paths for transitions between adjacent flight plan legs.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param startIndex The index of the first leg for which to calculate transitions.
         * @param count The total number of legs for which to calculate transitions.
         * @param state The airplane state to use for calculations.
         */
        calculate(legs, startIndex, count, state) {
            const endIndex = startIndex + count;
            let currentIndex = startIndex;
            while (currentIndex < endIndex) {
                const fromLeg = legs[currentIndex];
                const toLeg = legs[currentIndex + 1];
                const fromLegCalc = fromLeg === null || fromLeg === void 0 ? void 0 : fromLeg.calculated;
                const toLegCalc = toLeg === null || toLeg === void 0 ? void 0 : toLeg.calculated;
                if (fromLegCalc
                    && toLegCalc
                    && !fromLegCalc.endsInDiscontinuity) {
                    const fromVector = fromLegCalc.flightPath[fromLegCalc.flightPath.length - 1];
                    const toVector = toLegCalc.flightPath[0];
                    if (fromVector && toVector
                        && !BitFlags.isAny(fromVector.flags, exports.FlightPathVectorFlags.Discontinuity)
                        && !BitFlags.isAny(toVector.flags, exports.FlightPathVectorFlags.Discontinuity)
                        && FlightPathLegToLegCalculator.canCalculateTransition(fromLegCalc.egress)
                        && FlightPathLegToLegCalculator.canCalculateTransition(toLegCalc.ingress)) {
                        // There are three types of leg-to-leg junctions we must handle:
                        // 1) Junction between two great-circle vectors (tracks).
                        // 2) Junction between a great-circle vector (track) and a small-circle vector (turn).
                        // 3) Junction between two small-circle vectors (turns).
                        const isFromVectorGreatCircle = FlightPathUtils.isVectorGreatCircle(fromVector);
                        const isToVectorGreatCircle = FlightPathUtils.isVectorGreatCircle(toVector);
                        if (isFromVectorGreatCircle && isToVectorGreatCircle) {
                            currentIndex += this.calculateTrackTrackTransition(legs, currentIndex, currentIndex + 1, endIndex, state, fromVector, toVector, true);
                            continue;
                        }
                        else if (isFromVectorGreatCircle) {
                            currentIndex += this.calculateTrackTurnTransition(legs, currentIndex, currentIndex + 1, endIndex, state, fromVector, toVector, false);
                            continue;
                        }
                        else if (isToVectorGreatCircle) {
                            currentIndex += this.calculateTrackTurnTransition(legs, currentIndex, currentIndex + 1, endIndex, state, fromVector, toVector, true);
                            continue;
                        }
                        else ;
                    }
                }
                if (fromLegCalc && fromLegCalc.egress.length > 0 && FlightPathLegToLegCalculator.canCalculateTransition(fromLegCalc.egress)) {
                    fromLegCalc.egress.length = 0;
                    fromLegCalc.egressJoinIndex = -1;
                }
                if (toLegCalc && toLegCalc.ingress.length > 0 && FlightPathLegToLegCalculator.canCalculateTransition(toLegCalc.ingress)) {
                    toLegCalc.ingress.length = 0;
                    toLegCalc.ingressJoinIndex = -1;
                }
                currentIndex++;
            }
        }
        /**
         * Sets an empty transition between two adjacent flight plan legs. This will erase all egress transition vectors from
         * the FROM leg and all ingress transition vectors from the TO leg.
         * @param fromLegCalc The flight path calculations for the transition's FROM leg.
         * @param toLegCalc The flight path calculations for the transition's TO leg.
         */
        setEmptyTransition(fromLegCalc, toLegCalc) {
            fromLegCalc.egress.length = 0;
            fromLegCalc.egressJoinIndex = -1;
            toLegCalc.ingress.length = 0;
            toLegCalc.ingressJoinIndex = -1;
        }
        /**
         * Sets the transition between two adjacent flight plan legs to an anticipated turn.
         * @param fromLegCalc The flight path calculations for the transition's FROM leg.
         * @param toLegCalc The flight path calculations for the transition's TO leg.
         * @param turnRadius The radius of the turn, in great-arc radians.
         * @param turnDirection The direction of the turn.
         * @param turnCenter The center of the turn.
         * @param turnStart The start point of the turn.
         * @param turnMiddle The midpoint of the turn.
         * @param turnEnd The end point of the turn
         * @param setIngressEgressArrayLengths Whether to remove extra vectors from the ingress and egress vector arrays used
         * for the transition after the anticipated turn vectors have been added.
         */
        setAnticipatedTurn(fromLegCalc, toLegCalc, turnRadius, turnDirection, turnCenter, turnStart, turnMiddle, turnEnd, setIngressEgressArrayLengths) {
            const turnCircle = FlightPathUtils.getTurnCircle(turnCenter, turnRadius, turnDirection, this.setAnticipatedTurnCache.geoCircle[0]);
            const flags = exports.FlightPathVectorFlags.LegToLegTurn | exports.FlightPathVectorFlags.AnticipatedTurn;
            this.setEgressVector(fromLegCalc, turnCircle, turnStart, turnMiddle, flags, setIngressEgressArrayLengths);
            this.setIngressVector(toLegCalc, turnCircle, turnMiddle, turnEnd, flags, setIngressEgressArrayLengths);
        }
        /**
         * Sets the egress transition of a flight plan leg to a single vector.
         * @param legCalc The flight path calculations for the transition's leg.
         * @param path A GeoCircle that defines the path of the vector.
         * @param start The start point of the vector.
         * @param end The end point of the vector.
         * @param flags The flags to set on the vector.
         * @param setEgressArrayLength Whether to remove extra vectors from the egress vector array after the vector has been
         * added.
         */
        setEgressVector(legCalc, path, start, end, flags, setEgressArrayLength) {
            var _a;
            var _b;
            const vector = (_a = (_b = legCalc.egress)[0]) !== null && _a !== void 0 ? _a : (_b[0] = FlightPathUtils.createEmptyVector());
            if (setEgressArrayLength) {
                legCalc.egress.length = 1;
            }
            legCalc.egressJoinIndex = legCalc.flightPath.length - 1;
            const joinedVector = legCalc.flightPath[legCalc.egressJoinIndex];
            flags |= joinedVector.flags & exports.FlightPathVectorFlags.Fallback;
            FlightPathUtils.setVectorFromCircle(vector, path, start, end, flags, joinedVector.heading, joinedVector.isHeadingTrue);
        }
        /**
         * Sets the ingress transition of a flight plan leg to a single vector.
         * @param legCalc The flight path calculations for the transition's leg.
         * @param path A GeoCircle that defines the path of the vector.
         * @param start The start point of the vector.
         * @param end The end point of the vector.
         * @param flags The flags to set on the vector.
         * @param setIngressArrayLength Whether to remove extra vectors from the ingress vector array after the vector has
         * been added.
         */
        setIngressVector(legCalc, path, start, end, flags, setIngressArrayLength) {
            var _a;
            var _b;
            const ingress = (_a = (_b = legCalc.ingress)[0]) !== null && _a !== void 0 ? _a : (_b[0] = FlightPathUtils.createEmptyVector());
            if (setIngressArrayLength) {
                legCalc.ingress.length = 1;
            }
            legCalc.ingressJoinIndex = 0;
            const joinedVector = legCalc.flightPath[legCalc.ingressJoinIndex];
            flags |= joinedVector.flags & exports.FlightPathVectorFlags.Fallback;
            FlightPathUtils.setVectorFromCircle(ingress, path, start, end, flags, joinedVector.heading, joinedVector.isHeadingTrue);
        }
        /**
         * Calculates a leg-to-leg transition between two great-circle ("track") vectors. In calculating the specified
         * transition, this method may also calculate a sequence of consecutive transitions following the specified
         * transition.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param fromIndex The index of the transition's FROM leg.
         * @param toIndex The index of the transition's TO leg.
         * @param endIndex The index of the flight plan leg at which to stop calculating transitions. The last transition to
         * be calculated will be between the this leg and the previous leg.
         * @param state The airplane state to use for calculations.
         * @param fromVector The last base flight path vector of the transition's FROM leg (the FROM vector).
         * @param toVector The first base flight path vector of the transition's TO leg (the TO vector).
         * @param isRestrictedByPrevTransition Whether the FROM leg's egress transition is restricted by the leg's ingress
         * transition.
         * @param previousTanTheta The tangent of the theta angle of the previous transition's anticipated turn. Theta is
         * defined as the (acute) angle between either the FROM vector or the TO vector's path and the great circle passing
         * through the point where the FROM and TO vectors meet and the center of the anticipated turn. If there is no
         * previous transition, the previous transition is not an anticipated turn, or the previous transition's FROM and
         * TO vectors are not both great-circle paths, then this value should be left undefined.
         * @returns The number of consecutive leg-to-leg transitions calculated by this method.
         */
        calculateTrackTrackTransition(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isRestrictedByPrevTransition, previousTanTheta) {
            const fromLegCalc = legs[fromIndex].calculated;
            const toLegCalc = legs[toIndex].calculated;
            if (fromVector.distance <= GeoMath.ANGULAR_TOLERANCE || toVector.distance <= GeoMath.ANGULAR_TOLERANCE) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return 1;
            }
            const fromVectorEnd = this.trackTrackCache.geoPoint[0].set(fromVector.endLat, fromVector.endLon);
            const toVectorStart = this.trackTrackCache.geoPoint[1].set(toVector.startLat, toVector.startLon);
            // If the TO vector doesn't start where the FROM vector ends, then there can be no transition. We use a rather
            // large tolerance here (~60 meters) to accommodate imprecise nav data and floating point errors during base flight
            // path calculation.
            if (!fromVectorEnd.equals(toVectorStart, 1e-5)) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return 1;
            }
            const fromVectorPath = FlightPathUtils.setGeoCircleFromVector(fromVector, this.trackTrackCache.geoCircle[0]);
            const toVectorPath = FlightPathUtils.setGeoCircleFromVector(toVector, this.trackTrackCache.geoCircle[1]);
            const trackAngleDiff = Vec3Math.unitAngle(fromVectorPath.center, toVectorPath.center) * Avionics.Utils.RAD2DEG;
            if (trackAngleDiff < 1) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return 1;
            }
            else if (trackAngleDiff > 175) {
                return this.computeTrackTrackCourseReversal(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector);
            }
            return this.calculateTrackTrackAnticipatedTurn(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isRestrictedByPrevTransition, previousTanTheta);
        }
        /**
         * Calculates a leg-to-leg course reversal transition between two great-circle ("track") vectors. In calculating the
         * specified transition, this method may also calculate a sequence of consecutive transitions following the specified
         * transition.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param fromIndex The index of the transition's FROM leg.
         * @param toIndex The index of the transition's TO leg.
         * @param endIndex The index of the flight plan leg at which to stop calculating transitions. The last transition to
         * be calculated will be between the this leg and the previous leg.
         * @param state The airplane state to use for calculations.
         * @param fromVector The last base flight path vector of the transition's FROM leg (the FROM vector).
         * @param toVector The first base flight path vector of the transition's TO leg (the TO vector).
         * @returns The number of consecutive leg-to-leg transitions calculated by this method.
         */
        computeTrackTrackCourseReversal(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector) {
            var _a;
            let calculatedCount = 1;
            const fromLegCalc = legs[fromIndex].calculated;
            const toLegCalc = legs[toIndex].calculated;
            fromLegCalc.egress.length = 0;
            fromLegCalc.egressJoinIndex = -1;
            let toVectorStartVec;
            let toVectorPath;
            // Allow the course reversal to "cut" into the TO vector. In other words, the course reversal is allowed to
            // intercept the TO leg in the middle of the leg.
            let courseReversalEndDistance = UnitType.METER.convertTo(toVector.distance, UnitType.GA_RADIAN);
            // If the TO leg only has one base flight path vector, then we need to make sure the course reversal doesn't cut
            // into the leg past the point where the egress joins the vector.
            if (toLegCalc.flightPath.length === 1) {
                let needCheckEgressJoin = true;
                // We need to check if the TO leg's egress transition is going to be recalculated with the current round of
                // calculations. Depending on if and how the transition is going to be recalculated, we may need not need to
                // check the egress vectors or we may need to pre-compute the egress transition.
                if (toIndex < endIndex) {
                    const nextLegCalc = (_a = legs[toIndex + 1]) === null || _a === void 0 ? void 0 : _a.calculated;
                    if (nextLegCalc) {
                        if (FlightPathLegToLegCalculator.canCalculateTransition(toLegCalc.egress)
                            && FlightPathLegToLegCalculator.canCalculateTransition(nextLegCalc.ingress)) {
                            if (nextLegCalc.flightPath.length > 0) {
                                const nextVector = nextLegCalc.flightPath[0];
                                if (FlightPathUtils.isVectorGreatCircle(nextVector)) {
                                    // If the TO vector of the next leg-to-leg transition is a great circle, then we are
                                    // allowed to pre-compute the next transition.
                                    calculatedCount += this.calculateTrackTrackTransition(legs, toIndex, toIndex + 1, endIndex, state, toVector, nextVector, false);
                                    // We still need to check the egress here since we don't know where the egress joins until after it is
                                    // calculated.
                                }
                                else {
                                    // If the TO vector of the next leg-to-leg transition to share a vector with the current leg-toleg
                                    // transition is not a great circle, then we know that the distance from the start of the vector to
                                    // where the egress joins is guaranteed to be at least half the distance of the vector.
                                    courseReversalEndDistance = UnitType.METER.convertTo(toVector.distance / 2, UnitType.GA_RADIAN);
                                    needCheckEgressJoin = false;
                                }
                            }
                            else {
                                // If the next leg after the TO leg has no base flight path vectors, then the recalculation will erase
                                // any egress vectors on the TO leg, so there is no need to check the egress vectors.
                                needCheckEgressJoin = false;
                            }
                        }
                    }
                    else {
                        // If there is no leg that has a calculated flight path after the TO leg and the TO leg's egress is
                        // recalculated, then it is guaranteed to be erased, in which case there is no need to check the egress
                        // vectors.
                        if (FlightPathLegToLegCalculator.canCalculateTransition(toLegCalc.egress)) {
                            needCheckEgressJoin = false;
                        }
                    }
                }
                if (needCheckEgressJoin && toLegCalc.egress.length > 0 && toLegCalc.egressJoinIndex === 0) {
                    toVectorStartVec = GeoPoint.sphericalToCartesian(toVector.startLat, toVector.startLon, this.trackTrackCache.vec3[1]);
                    toVectorPath = FlightPathUtils.setGeoCircleFromVector(toVector, this.trackTrackCache.geoCircle[1]);
                    const egressJoinDistance = toVectorPath.distanceAlong(toVectorStartVec, GeoPoint.sphericalToCartesian(toLegCalc.egress[0].startLat, toLegCalc.egress[0].startLon, this.trackTrackCache.vec3[2]), Math.PI);
                    courseReversalEndDistance = egressJoinDistance > Math.PI + GeoMath.ANGULAR_TOLERANCE ? 0 : egressJoinDistance;
                }
            }
            const fromVectorEndVec = GeoPoint.sphericalToCartesian(fromVector.endLat, fromVector.endLon, this.trackTrackCache.vec3[0]);
            const fromVectorPath = FlightPathUtils.setGeoCircleFromVector(fromVector, this.trackTrackCache.geoCircle[0]);
            toVectorStartVec !== null && toVectorStartVec !== void 0 ? toVectorStartVec : (toVectorStartVec = GeoPoint.sphericalToCartesian(toVector.startLat, toVector.startLon, this.trackTrackCache.vec3[1]));
            toVectorPath !== null && toVectorPath !== void 0 ? toVectorPath : (toVectorPath = FlightPathUtils.setGeoCircleFromVector(toVector, this.trackTrackCache.geoCircle[1]));
            const courseReversalEndVec = toVectorPath.offsetDistanceAlong(toVectorStartVec, courseReversalEndDistance, this.trackTrackCache.vec3[2], Math.PI);
            const fromVectorCourse = fromVectorPath.bearingAt(fromVectorEndVec);
            const toVectorCourse = toVectorPath.bearingAt(fromVectorEndVec);
            const turnDirection = MathUtils.angularDistanceDeg(fromVectorCourse, toVectorCourse, 1) > 180 ? 'left' : 'right';
            const vectorCount = this.procTurnVectorBuilder.build(toLegCalc.ingress, 0, fromVectorEndVec, fromVectorPath, courseReversalEndVec, toVectorPath, fromVectorCourse + (turnDirection === 'left' ? -45 : 45), state.desiredCourseReversalTurnRadius.asUnit(UnitType.METER), turnDirection, fromVectorCourse, toVectorCourse, exports.FlightPathVectorFlags.LegToLegTurn | exports.FlightPathVectorFlags.CourseReversal, true, toLegCalc.flightPath[0].heading, toLegCalc.flightPath[0].isHeadingTrue);
            toLegCalc.ingress.length = vectorCount;
            toLegCalc.ingressJoinIndex = 0;
            return calculatedCount;
        }
        /**
         * Calculates a leg-to-leg anticipated turn transition between two great-circle ("track") vectors. In calculating the
         * specified transition, this method may also calculate a sequence of consecutive transitions following the specified
         * transition.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param fromIndex The index of the transition's FROM leg.
         * @param toIndex The index of the transition's TO leg.
         * @param endIndex The index of the flight plan leg at which to stop calculating transitions. The last transition to
         * be calculated will be between the this leg and the previous leg.
         * @param state The airplane state to use for calculations.
         * @param fromVector The last base flight path vector of the transition's FROM leg (the FROM vector).
         * @param toVector The first base flight path vector of the transition's TO leg (the TO vector).
         * @param isRestrictedByPrevTransition Whether the FROM leg's egress transition is restricted by the leg's ingress
         * transition.
         * @param previousTanTheta The tangent of the theta angle of the previous transition's anticipated turn. Theta is
         * defined as the (acute) angle between either the FROM vector or the TO vector's path and the great circle passing
         * through the point where the FROM and TO vectors meet and the center of the anticipated turn. If there is no
         * previous transition, the previous transition is not an anticipated turn, or the previous transition's FROM and
         * TO vectors are not both great-circle paths, then this value should be left undefined.
         *
         * If this value is defined, `isRestrictedByPrevTransition` is true, and the current anticipated turn would infringe
         * on the previous anticipated turn, then the anticipated distance of the current turn will be adjusted to maximize
         * the radius of the smaller of the two turns assuming the current turn starts exactly where the previous turn ends.
         * @returns The number of consecutive leg-to-leg transitions calculated by this method.
         */
        calculateTrackTrackAnticipatedTurn(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isRestrictedByPrevTransition, previousTanTheta) {
            var _a, _b;
            let calculatedCount = 1;
            const fromLegCalc = legs[fromIndex].calculated;
            const toLegCalc = legs[toIndex].calculated;
            const fromVectorEnd = this.trackTrackCache.geoPoint[0].set(fromVector.endLat, fromVector.endLon);
            // From this point on, to simplify calculations, we will assume that the FROM and TO paths intersect at the end
            // point of the FROM vector. (This may not actually be the case since the end point of the FROM vector and the
            // start point of the TO vector are allowed to be different within some tolerance. Accumulated floating point
            // errors can also lead to violations of this assumption.) Using this assumption, calculate the position of the
            // anticipated turn circle and the start, end, and midpoints of the turn.
            const fromVectorCourse = fromVectorEnd.bearingFrom(fromVector.startLat, fromVector.startLon);
            const toVectorCourse = fromVectorEnd.bearingTo(toVector.endLat, toVector.endLon);
            const courseAngleDiff = MathUtils.angularDistanceDeg(fromVectorCourse, toVectorCourse, 0);
            if (courseAngleDiff < 1) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return calculatedCount;
            }
            const theta = (180 - courseAngleDiff) / 2;
            const tanTheta = Math.tan(theta * Avionics.Utils.DEG2RAD);
            // D is defined as the distance along the FROM or TO vectors from the start or end of the anticipated turn to the
            // turn vertex (where the FROM and TO vectors meet). In other words, D is the along-track anticipated turn
            // distance.
            const desiredD = Math.asin(MathUtils.clamp(Math.tan(state.desiredTurnAnticipationTurnRadius.asUnit(UnitType.GA_RADIAN)) / tanTheta, -1, 1));
            let restrictedD = Infinity;
            if (isRestrictedByPrevTransition) {
                if (previousTanTheta === undefined) {
                    // D is not restricted by a previous anticipated turn. Now we need to check if there is an ingress transition
                    // on the FROM leg and if it shares a common flight path vector with the one involved in the anticipated turn
                    // currently being calculated.
                    if (fromLegCalc.ingress.length > 0 && fromLegCalc.ingressJoinIndex === fromLegCalc.flightPath.length - 1) {
                        const fromVectorPath = FlightPathUtils.setGeoCircleFromVector(fromVector, this.trackTrackCache.geoCircle[0]);
                        const lastIngressVector = fromLegCalc.ingress[fromLegCalc.ingress.length - 1];
                        const ingressJoinDistance = fromVectorPath.distanceAlong(GeoPoint.sphericalToCartesian(lastIngressVector.endLat, lastIngressVector.endLon, this.trackTrackCache.vec3[0]), fromVectorEnd, Math.PI);
                        restrictedD = ingressJoinDistance > Math.PI + GeoMath.ANGULAR_TOLERANCE ? 0 : ingressJoinDistance;
                    }
                }
                else {
                    // D is restricted by a previous anticipated turn. The values of D_current and D_previous are restricted such
                    // that their sum cannot exceed the total length of their shared vector (the current FROM vector). Therefore,
                    // we set the maximum value of D_current such that at D_current(max), the radius of the current anticipated
                    // turn equals the radius of the previous turn. This will maximize the radius of the smaller of the current
                    // anticipated turn and the previous anticipated turn.
                    const tanThetaRatio = previousTanTheta / tanTheta;
                    const totalD = UnitType.METER.convertTo(fromVector.distance, UnitType.GA_RADIAN);
                    const cosTotalD = Math.cos(totalD);
                    let prevTurnRestrictedD = Math.acos(MathUtils.clamp((tanThetaRatio * cosTotalD + 1) / Math.sqrt(tanThetaRatio * tanThetaRatio + 2 * tanThetaRatio * cosTotalD + 1), -1, 1));
                    if (prevTurnRestrictedD > totalD) {
                        prevTurnRestrictedD = Math.PI - prevTurnRestrictedD;
                    }
                    restrictedD = prevTurnRestrictedD;
                }
            }
            // If the TO leg only has one base flight path vector, then we need to scan forward in the leg sequence to compute
            // any restrictions on D imposed by later transitions.
            if (toLegCalc.flightPath.length === 1) {
                let nextTransitionRestrictedD;
                // We need to check if the TO leg's egress transition is going to be recalculated with the current round of
                // calculations. Depending on if and how the transition is going to be recalculated, we may need not need to
                // check the egress vectors or we may need to pre-compute the egress transition.
                if (toIndex < endIndex && FlightPathLegToLegCalculator.canCalculateTransition(toLegCalc.egress)) {
                    const nextLegCalc = (_a = legs[toIndex + 1]) === null || _a === void 0 ? void 0 : _a.calculated;
                    if (nextLegCalc && nextLegCalc.flightPath.length > 0 && FlightPathLegToLegCalculator.canCalculateTransition(nextLegCalc.ingress)) {
                        const nextVector = nextLegCalc.flightPath[0];
                        if (FlightPathUtils.isVectorGreatCircle(nextVector)) {
                            // If the TO vector of the next leg-to-leg transition is a great circle, then we are allowed to pre-compute
                            // the next transition.
                            calculatedCount += this.calculateTrackTrackTransition(legs, toIndex, toIndex + 1, endIndex, state, toVector, nextVector, true, tanTheta);
                        }
                        else {
                            // If the TO vector of the next leg-to-leg transition to share a vector with the current leg-to-leg
                            // transition is not a great circle, then we know that the distance from the start of the vector to where
                            // the egress joins is guaranteed to be at least half the distance of the vector.
                            nextTransitionRestrictedD = UnitType.METER.convertTo(toVector.distance / 2, UnitType.GA_RADIAN);
                        }
                    }
                }
                // If we haven't defined a restriction on D from the next transition yet, then check if the TO leg has a
                // calculated egress. If it does, then set the restriction to the distance from the start of the TO vector to
                // where the egress joins the vector.
                if (nextTransitionRestrictedD === undefined) {
                    if (toLegCalc.egress.length > 0 && toLegCalc.egressJoinIndex === 0) {
                        const toVectorPath = FlightPathUtils.setGeoCircleFromVector(toVector, this.trackTrackCache.geoCircle[0]);
                        const egressJoinDistance = toVectorPath.distanceAlong(GeoPoint.sphericalToCartesian(toVector.startLat, toVector.startLon, this.trackTrackCache.vec3[0]), GeoPoint.sphericalToCartesian(toLegCalc.egress[0].startLat, toLegCalc.egress[0].startLon, this.trackTrackCache.vec3[1]), Math.PI);
                        nextTransitionRestrictedD = egressJoinDistance > Math.PI + GeoMath.ANGULAR_TOLERANCE ? 0 : egressJoinDistance;
                    }
                    else {
                        nextTransitionRestrictedD = Infinity;
                    }
                }
                restrictedD = Math.min(restrictedD, nextTransitionRestrictedD);
            }
            const D = Math.min(desiredD, restrictedD, UnitType.METER.convertTo(fromVector.distance, UnitType.GA_RADIAN), UnitType.METER.convertTo(toVector.distance, UnitType.GA_RADIAN));
            // The distance from the turn vertex to the center of the turn.
            const H = Math.atan(Math.tan(D) / Math.cos(theta * Avionics.Utils.DEG2RAD));
            const turnRadiusRad = desiredD === D
                ? state.desiredTurnAnticipationTurnRadius.asUnit(UnitType.GA_RADIAN)
                : Math.atan(Math.sin(D) * tanTheta);
            if (D <= GeoMath.ANGULAR_TOLERANCE || turnRadiusRad <= GeoMath.ANGULAR_TOLERANCE) {
                // Prevent zero-length turns.
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return calculatedCount;
            }
            // We need to reset the GeoPoint because the potential call to calculateTrackTrackTransition() above can overwrite
            // it.
            fromVectorEnd.set(fromVector.endLat, fromVector.endLon);
            const turnDirection = (_b = FlightPathUtils.getShortestTurnDirection(fromVectorCourse, toVectorCourse)) !== null && _b !== void 0 ? _b : 'right';
            const turnBisectorBearing = toVectorCourse + theta * (turnDirection === 'left' ? -1 : 1);
            const turnCenterVec = fromVectorEnd.offset(turnBisectorBearing, H, this.trackTrackCache.geoPoint[1]).toCartesian(this.trackTrackCache.vec3[0]);
            const turnStart = fromVectorEnd.offset(fromVectorCourse, -D, this.trackTrackCache.geoPoint[1]);
            const turnMiddle = fromVectorEnd.offset(turnBisectorBearing, H - turnRadiusRad, this.trackTrackCache.geoPoint[2]);
            const turnEnd = fromVectorEnd.offset(toVectorCourse, D, this.trackTrackCache.geoPoint[3]);
            this.setAnticipatedTurn(fromLegCalc, toLegCalc, turnRadiusRad, turnDirection, turnCenterVec, turnStart, turnMiddle, turnEnd, true);
            return calculatedCount;
        }
        /**
         * Calculates a leg-to-leg transition between a great-circle ("track") vector and a small-circle ("turn") vector.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param fromIndex The index of the transition's FROM leg.
         * @param toIndex The index of the transition's TO leg.
         * @param endIndex The index of the flight plan leg at which to stop calculating transitions. The last transition to
         * be calculated will be between the this leg and the previous leg.
         * @param state The airplane state to use for calculations.
         * @param fromVector The last base flight path vector of the transition's FROM leg (the FROM vector).
         * @param toVector The first base flight path vector of the transition's TO leg (the TO vector).
         * @param isTurnFirst Whether the FROM vector is the turn vector.
         * @returns The number of consecutive leg-to-leg transitions calculated by this method.
         */
        calculateTrackTurnTransition(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isTurnFirst) {
            const fromLeg = legs[fromIndex];
            const toLeg = legs[toIndex];
            const fromLegCalc = fromLeg.calculated;
            const toLegCalc = toLeg.calculated;
            if (fromVector.distance <= GeoMath.ANGULAR_TOLERANCE || toVector.distance <= GeoMath.ANGULAR_TOLERANCE) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return 1;
            }
            const fromVectorEnd = this.trackTurnCache.geoPoint[0].set(fromVector.endLat, fromVector.endLon);
            const toVectorStart = this.trackTurnCache.geoPoint[1].set(toVector.startLat, toVector.startLon);
            const areLegsContinuous = fromVectorEnd.equals(toVectorStart, 1e-5);
            if (!areLegsContinuous) {
                // The FROM leg does not end within ~60 meters of the start of the TO leg. We will set an empty transition
                // UNLESS either the FROM or TO leg is an AF or RF leg. These leg types often end up being somewhat discontinuous
                // with the preceding or proceeding leg, because the arcs can be slightly offset from the intended origin and/or
                // terminator fixes.
                let shouldQuit = true;
                if (fromLeg.leg.type === exports.LegType.AF
                    || fromLeg.leg.type === exports.LegType.RF
                    || toLeg.leg.type === exports.LegType.AF
                    || toLeg.leg.type === exports.LegType.RF) {
                    shouldQuit = false;
                }
                if (shouldQuit) {
                    this.setEmptyTransition(fromLegCalc, toLegCalc);
                    return 1;
                }
            }
            const turnVector = isTurnFirst ? fromVector : toVector;
            const fromVectorPath = FlightPathUtils.setGeoCircleFromVector(fromVector, this.trackTurnCache.geoCircle[0]);
            const toVectorPath = FlightPathUtils.setGeoCircleFromVector(toVector, this.trackTurnCache.geoCircle[1]);
            const turnPath = isTurnFirst ? fromVectorPath : toVectorPath;
            const turnRadiusRad = FlightPathUtils.getTurnRadiusFromCircle(turnPath);
            const turnCircle = this.trackTurnCache.geoCircle[2].set(turnPath.center, turnPath.radius);
            if (turnCircle.radius > MathUtils.HALF_PI) {
                turnCircle.reverse();
            }
            const trackPath = isTurnFirst ? toVectorPath : fromVectorPath;
            // Calculate whether the turn intersects the track. If they don't (or if they are entirely coincident), then we
            // will immediately bail out.
            const intersections = this.trackTurnCache.intersection[0];
            const trackTurnIntersectionCount = turnCircle.intersection(trackPath, intersections);
            if (trackTurnIntersectionCount === 0) {
                this.setEmptyTransition(fromLegCalc, toLegCalc);
                return 1;
            }
            const fromVectorEndVec = fromVectorEnd.toCartesian(this.trackTurnCache.vec3[0]);
            const toVectorStartVec = toVectorStart.toCartesian(this.trackTurnCache.vec3[1]);
            const fromVectorHalfDistanceRad = UnitType.METER.convertTo(fromVector.distance / 2, UnitType.GA_RADIAN);
            const toVectorHalfDistanceRad = UnitType.METER.convertTo(toVector.distance / 2, UnitType.GA_RADIAN);
            const intersectionVec = this.trackTurnCache.vec3[2];
            let intersectionFromVectorEndOffset = 0;
            let intersectionToVectorStartOffset = 0;
            let isReversal = false;
            let isTransitionInsideTurn = false;
            let transitionTurnRadiusRad = 0;
            let transitionTurnDirection = 'right';
            let turnCircleOffsetSign = 1;
            let trackPathOffsetSign = 1;
            if (trackTurnIntersectionCount === 1) {
                // The turn circle and track path are tangent.
                if (areLegsContinuous) {
                    // The FROM leg ends within ~60 meters of the start of the TO leg, so we will just proceed as if the FROM
                    // vector ends at exactly at the start of the TO vector and both points are coincident with the
                    // intersection between the FROM and TO vectors.
                    Vec3Math.copy(fromVectorEndVec, intersectionVec);
                    intersectionToVectorStartOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(toVectorPath, intersectionVec, toVectorStartVec);
                }
                else {
                    // The FROM leg does not end within ~60 meters of the start of the TO leg. Therefore, we need to check
                    // whether the intersection between the FROM and TO vectors is valid.
                    Vec3Math.copy(intersections[0], intersectionVec);
                    intersectionFromVectorEndOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(fromVectorPath, fromVectorEndVec, intersectionVec);
                    intersectionToVectorStartOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(toVectorPath, intersectionVec, toVectorStartVec);
                    if (!this.isTrackTurnIntersectionValid(intersectionVec, fromVectorPath, fromVectorEndVec, fromVectorHalfDistanceRad, intersectionFromVectorEndOffset, toVectorPath, toVectorStartVec, toVectorHalfDistanceRad, intersectionToVectorStartOffset)) {
                        this.setEmptyTransition(fromLegCalc, toLegCalc);
                        return 1;
                    }
                }
                // Whether the TO and FROM vectors are oriented in the same direction at the tangent point (i.e. whether their
                // paths are parallel).
                const isForward = Vec3Math.dot(GeoCircle.getGreatCircleNormal(intersectionVec, turnPath, this.trackTurnCache.vec3[3]), trackPath.center) >= 0;
                if (isForward) {
                    // The FROM and TO vectors are parallel at the tangent point. In this case no anticipated turn is needed
                    // between the FROM and TO vectors.
                    transitionTurnRadiusRad = 0;
                }
                else {
                    // The FROM and TO vectors are antiparallel at the tangent point. In this case the plane needs to make a
                    // 180-degree turn. We will set the parameters of the turn to ensure that it begins after the end of the FROM
                    // vector and ends before the start of the TO vector.
                    isReversal = true;
                    isTransitionInsideTurn = false;
                    transitionTurnRadiusRad = state.desiredTurnAnticipationTurnRadius.asUnit(UnitType.GA_RADIAN);
                    turnCircleOffsetSign = 1;
                    trackPathOffsetSign = trackPath.encircles(turnCircle.center) ? -1 : 1;
                }
            }
            else {
                // The turn circle and track path are secant.
                let turnStartVec;
                let turnEndVec;
                if (areLegsContinuous) {
                    // The FROM leg ends within ~60 meters of the start of the TO leg, so we will just proceed as if the FROM
                    // vector ends at exactly at the start of the TO vector and both points are coincident with the intersection
                    // between the FROM and TO vectors.
                    Vec3Math.copy(fromVectorEndVec, intersectionVec);
                    turnStartVec = GeoPoint.sphericalToCartesian(turnVector.startLat, turnVector.startLon, this.trackTurnCache.vec3[3]);
                    turnEndVec = GeoPoint.sphericalToCartesian(turnVector.endLat, turnVector.endLon, this.trackTurnCache.vec3[4]);
                }
                else {
                    // The FROM leg does not end within ~60 meters of the start of the TO leg. Therefore, we need to check whether
                    // either of the intersections between the FROM and TO vectors is valid.
                    const intersection0FromVectorEndOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(fromVectorPath, fromVectorEndVec, intersections[0]);
                    const intersection0ToVectorStartOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(toVectorPath, intersections[0], toVectorStartVec);
                    const intersection1FromVectorEndOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(fromVectorPath, fromVectorEndVec, intersections[1]);
                    const intersection1ToVectorStartOffset = FlightPathLegToLegCalculator.getAlongCircleOffset(toVectorPath, intersections[1], toVectorStartVec);
                    const isIntersection0Valid = this.isTrackTurnIntersectionValid(intersections[0], fromVectorPath, fromVectorEndVec, fromVectorHalfDistanceRad, intersection0FromVectorEndOffset, toVectorPath, toVectorStartVec, toVectorHalfDistanceRad, intersection0ToVectorStartOffset);
                    const isIntersection1Valid = this.isTrackTurnIntersectionValid(intersections[1], fromVectorPath, fromVectorEndVec, fromVectorHalfDistanceRad, intersection1FromVectorEndOffset, toVectorPath, toVectorStartVec, toVectorHalfDistanceRad, intersection1ToVectorStartOffset);
                    if (!isIntersection0Valid && !isIntersection1Valid) {
                        this.setEmptyTransition(fromLegCalc, toLegCalc);
                        return 1;
                    }
                    let intersectionIndex;
                    if (!isIntersection0Valid) {
                        intersectionIndex = 1;
                    }
                    else if (!isIntersection1Valid) {
                        intersectionIndex = 0;
                    }
                    else {
                        // Both intersections are valid. We will bias toward intersections that lie after the end of the FROM vector
                        // and before the start of the TO vector.
                        if (intersection0FromVectorEndOffset + intersection0ToVectorStartOffset >= intersection1FromVectorEndOffset + intersection1ToVectorStartOffset) {
                            intersectionIndex = 0;
                        }
                        else {
                            intersectionIndex = 1;
                        }
                    }
                    if (intersectionIndex === 0) {
                        Vec3Math.copy(intersections[0], intersectionVec);
                        intersectionFromVectorEndOffset = intersection0FromVectorEndOffset;
                        intersectionToVectorStartOffset = intersection0ToVectorStartOffset;
                    }
                    else if (isIntersection1Valid) {
                        Vec3Math.copy(intersections[1], intersectionVec);
                        intersectionFromVectorEndOffset = intersection1FromVectorEndOffset;
                        intersectionToVectorStartOffset = intersection1ToVectorStartOffset;
                    }
                    if (isTurnFirst) {
                        turnStartVec = GeoPoint.sphericalToCartesian(turnVector.startLat, turnVector.startLon, this.trackTurnCache.vec3[3]);
                        turnEndVec = intersectionVec;
                    }
                    else {
                        turnStartVec = intersectionVec;
                        turnEndVec = GeoPoint.sphericalToCartesian(turnVector.endLat, turnVector.endLon, this.trackTurnCache.vec3[4]);
                    }
                }
                const turnDirection = FlightPathUtils.getTurnDirectionFromCircle(turnPath);
                const turnStartRadialNormal = GeoCircle.getGreatCircleNormal(turnCircle.center, turnStartVec, this.trackTurnCache.vec3[5]);
                const turnEndRadialNormal = GeoCircle.getGreatCircleNormal(turnCircle.center, turnEndVec, this.trackTurnCache.vec3[6]);
                const desiredTransitionTurnRadiusRad = state.desiredTurnAnticipationTurnRadius.asUnit(UnitType.GA_RADIAN);
                // The cosine of the angle between the track path and the great-circle path from the center of the turn to the
                // intersection point.
                const cosRadialCourseDiff = Vec3Math.dot(Vec3Math.normalize(Vec3Math.cross(turnCircle.center, intersectionVec, this.trackTurnCache.vec3[7]), this.trackTurnCache.vec3[7]), trackPath.center);
                if (Math.abs(cosRadialCourseDiff) <= 0.0174524064372836 /* 89-91 deg */) {
                    // The track path and the turn circle are nearly tangent. This means that the track path and turn circle are
                    // nearly parallel or antiparallel at the intersection point.
                    if (Vec3Math.dot(GeoCircle.getGreatCircleNormal(intersectionVec, turnPath, this.trackTurnCache.vec3[7]), trackPath.center) >= 0) {
                        // The track path and the turn circle are nearly parallel. In this case no anticipated turn is needed.
                        transitionTurnRadiusRad = 0;
                    }
                    else {
                        // The track path and the turn circle are nearly antiparallel. In this case the plane needs to make a
                        // 180-degree turn. We will set the parameters of the turn to ensure that it begins after the end of the FROM
                        // vector and ends before the start of the TO vector.
                        isReversal = true;
                        isTransitionInsideTurn = false;
                        transitionTurnRadiusRad = desiredTransitionTurnRadiusRad;
                        turnCircleOffsetSign = 1;
                        trackPathOffsetSign = trackPath.encircles(turnCircle.center) ? -1 : 1;
                    }
                }
                else {
                    transitionTurnDirection = FlightPathUtils.pathAngleDistance(fromVectorPath, toVectorPath, intersectionVec, 'left', Math.PI) <= Math.PI ? 'left' : 'right';
                    isTransitionInsideTurn = isTurnFirst ? cosRadialCourseDiff < 0 : cosRadialCourseDiff > 0;
                    // Now we must calculate the maximum allowed transition turn radius such that the transition turn does not
                    // start or end beyond the limits of the FROM or TO vectors. Define the track limit as the point along the
                    // track vector beyond which the transition cannot start or end. Define the turn limit as the point along the
                    // turn vector beyond which the transition cannot start or end. We will first convert the track limit to a
                    // pseudo-turn limit. The pseudo-turn limit is the point along the turn vector at which the transition turn
                    // will start or end if the transition turn starts or ends at the track limit. We will then take the more
                    // restrictive of the pseudo-turn limit and the actual turn limit. This maximally restrictive turn limit is
                    // then used to calculate the transition turn radius that would result in a transition turn that has an
                    // endpoint exactly at the limit.
                    const transitionTurnVertexRadialNormal = GeoCircle.getGreatCircleNormal(turnCircle.center, intersectionVec, this.trackTurnCache.vec3[7]);
                    // If the transition turn is inside the turn circle, then clamp the track limit distance to half the length of
                    // the track path within the turn circle. This is done because the transition turn radius is maximized when the
                    // transition turn starts or ends at the mid-point of the track path segment that lies within the turn circle.
                    const maxTrackLimitDistance = isTransitionInsideTurn
                        ? Math.atan(Math.abs(Vec3Math.dot(trackPath.center, transitionTurnVertexRadialNormal)) * Math.tan(turnRadiusRad))
                        : Infinity;
                    const trackLimitVec = this.trackTurnCache.vec3[8];
                    if (isTurnFirst) {
                        const trackLimitDistance = Math.min(toVectorHalfDistanceRad + intersectionToVectorStartOffset, maxTrackLimitDistance);
                        toVectorPath.offsetDistanceAlong(intersectionVec, trackLimitDistance, trackLimitVec, Math.PI);
                    }
                    else {
                        const trackLimitDistance = Math.min(fromVectorHalfDistanceRad + intersectionFromVectorEndOffset, maxTrackLimitDistance);
                        fromVectorPath.offsetDistanceAlong(intersectionVec, -trackLimitDistance, trackLimitVec, Math.PI);
                    }
                    // The great circle that passes through the center of the turn and is perpendicular to the track.
                    const trackPerpendicularDiameter = this.trackTurnCache.geoCircle[3].setAsGreatCircle(trackPath.center, turnCircle.center);
                    trackPerpendicularDiameter.intersection(turnCircle, intersections);
                    // Calculate the great circle that passes through the appropriate antipode and the track limit endpoint. The
                    // intersection (that is NOT the antipode) of this great circle with the turn circle is the pseudo-turn limit.
                    const intersectingPath = this.trackTurnCache.geoCircle[3].setAsGreatCircle(intersections[turnDirection === 'left' ? 0 : 1], trackLimitVec);
                    const turnIntersectionCount = intersectingPath.intersection(turnCircle, intersections);
                    let turnLimitAngularWidth = MathUtils.TWO_PI;
                    if (turnIntersectionCount > 0) {
                        turnLimitAngularWidth = Vec3Math.unitAngle(isTurnFirst ? turnEndRadialNormal : turnStartRadialNormal, GeoCircle.getGreatCircleNormal(turnCircle.center, intersections[0], this.trackTurnCache.vec3[8]));
                    }
                    // The angular width of the portion of the turn from the intersection to the mid-point of the turn vector.
                    const turnMidPointAngularWidth = isTurnFirst
                        ? fromVectorPath.angularWidth(fromVectorHalfDistanceRad + intersectionFromVectorEndOffset)
                        : toVectorPath.angularWidth(toVectorHalfDistanceRad + intersectionToVectorStartOffset);
                    turnLimitAngularWidth = Math.min(turnLimitAngularWidth, turnMidPointAngularWidth);
                    let transitionTurnRadiusLimitRad = 0;
                    if (turnLimitAngularWidth > 0) {
                        const turnLimitPointVec = turnCircle.offsetAngleAlong(isTurnFirst ? turnEndVec : turnStartVec, turnLimitAngularWidth * ((turnDirection === 'left') === isTurnFirst ? -1 : 1), this.trackTurnCache.vec3[8], Math.PI);
                        const turnLimitRadialPath = this.trackTurnCache.geoCircle[3].setAsGreatCircle(turnCircle.center, turnLimitPointVec);
                        // The angle between the track path (directed away from the turn at the point of intersection) and the
                        // radial from the turn center to the turn limit point.
                        const theta = Math.acos(MathUtils.clamp(Vec3Math.dot(turnLimitRadialPath.center, trackPath.center), -1, 1)
                            * (isTurnFirst == isTransitionInsideTurn ? -1 : 1));
                        if (theta >= MathUtils.HALF_PI) {
                            if (isTransitionInsideTurn) {
                                const D = Math.asin(Math.sin(Math.acos(Math.abs(MathUtils.clamp(Vec3Math.dot(trackPath.center, transitionTurnVertexRadialNormal), -1, 1))))
                                    * Math.sin(turnRadiusRad));
                                transitionTurnRadiusLimitRad = (turnRadiusRad - D) / 2;
                            }
                            else {
                                transitionTurnRadiusLimitRad = Math.PI;
                            }
                        }
                        else {
                            // Find the intersection of the radial from the turn center to the turn limit point with the track path. We
                            // choose the first intersection that is encountered when traveling along the radial, starting from the
                            // turn limit point.
                            const radialTrackIntersectionCount = turnLimitRadialPath.intersection(trackPath, intersections);
                            if (radialTrackIntersectionCount === 2) {
                                const thresholdNormal = GeoCircle.getGreatCircleNormal(turnLimitRadialPath.center, turnLimitPointVec, this.trackTurnCache.vec3[9]);
                                const turnLimitRadialTrackIntersection = Vec3Math.dot(intersections[0], thresholdNormal) >= 0
                                    ? intersections[0]
                                    : intersections[1];
                                // Cosine of the distance from the turn limit point to the intersection of the track path and the radial
                                // connecting the turn center to the turn limit point.
                                const cosD = MathUtils.clamp(Vec3Math.dot(turnLimitRadialTrackIntersection, turnLimitPointVec), -1, 1);
                                const sinTheta = Math.sin(theta);
                                const sign = isTransitionInsideTurn ? -1 : 1;
                                const denominator = 1 + sign * sinTheta * cosD;
                                if (denominator > 0) {
                                    const sinD = Math.sqrt(1 - cosD * cosD); // 0 <= D <= pi, so sin(D) must be non-negative.
                                    transitionTurnRadiusLimitRad = Math.atan((sinTheta * sinD) / denominator);
                                }
                                else {
                                    // This case technically should be impossible because it requires sin(theta) === 1, and we are guaranteed
                                    // that 0 <= theta < pi / 2, and therefore sin(theta) < 1. However, floating point error might invalidate
                                    // our assumptions, so we will still handle the case.
                                    transitionTurnRadiusLimitRad = MathUtils.HALF_PI;
                                }
                            }
                            else {
                                // If we are in this case, then the track path and the radial from the turn center to the turn limit
                                // point are parallel or antiparallel. This means that the turn limit point lies on the track path.
                                // Therefore, no transition turn of non-zero radius is possible.
                                transitionTurnRadiusLimitRad = 0;
                            }
                        }
                    }
                    transitionTurnRadiusRad = Math.min(desiredTransitionTurnRadiusRad, transitionTurnRadiusLimitRad);
                    turnCircleOffsetSign = isTransitionInsideTurn ? -1 : 1;
                    trackPathOffsetSign = transitionTurnDirection === 'left' ? -1 : 1;
                }
            }
            return this.calculateTrackTurnAnticipatedTurn(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isTurnFirst, fromVectorPath, toVectorPath, turnCircle, trackPath, intersectionVec, intersectionFromVectorEndOffset, intersectionToVectorStartOffset, isReversal, isTransitionInsideTurn, transitionTurnRadiusRad, transitionTurnDirection, turnCircleOffsetSign, trackPathOffsetSign);
        }
        /**
         * Checks if an intersection between an arc path and a track path is valid for computing turn anticipation between
         * arc and track vectors. The intersection is considered valid if and only if all the following conditions are true:
         * - The intersection is within one nautical mile of the end of the vector on which the turn begins.
         * - The intersection is within one nautical mile of the start of the vector on which the turn ends.
         * - The intersection is located after the mid-point of the vector on which the turn begins.
         * - The intersection is located before the mid-point of the vector on which the turn ends.
         * @param intersection The intersection to check.
         * @param fromVectorPath A geo circle defining the path of the vector on which the turn begins.
         * @param fromVectorEnd The end point of the vector on which the turn begins.
         * @param fromVectorHalfDistance Half of the distance covered by the vector on which the turn begins.
         * @param intersectionFromVectorEndOffset The along-vector offset distance, in great-arc radians, of the intersection
         * from the end point of the vector on which the turn begins. Positive offsets indicate the intersection is located
         * after the end point.
         * @param toVectorPath A geo circle defining the path of the vector on which the turn ends.
         * @param toVectorStart The start point of the vector on which the turn ends.
         * @param toVectorHalfDistance Half of the distance covered by the vector on which the turn ends.
         * @param intersectionToVectorStartOffset The along-vector offset distance, in great-arc radians, of the intersection
         * from the start point of the vector on which the turn ends. Positive offsets indicate the intersection is located
         * before the start point.
         * @returns Whether the specified intersection is valid for computing turn anticipation between arc and track
         * vectors.
         */
        isTrackTurnIntersectionValid(intersection, fromVectorPath, fromVectorEnd, fromVectorHalfDistance, intersectionFromVectorEndOffset, toVectorPath, toVectorStart, toVectorHalfDistance, intersectionToVectorStartOffset) {
            const fromVectorMidVec = fromVectorPath.offsetDistanceAlong(fromVectorEnd, -fromVectorHalfDistance, this.trackTurnIntersectionValidCache.vec3[0], Math.PI);
            const toVectorMidVec = toVectorPath.offsetDistanceAlong(toVectorStart, toVectorHalfDistance, this.trackTurnIntersectionValidCache.vec3[1], Math.PI);
            return (Math.abs(intersectionFromVectorEndOffset) <= 2.9e-4 // 2.9e-4 radians ~= 1 nautical mile
                && Math.abs(intersectionToVectorStartOffset) <= 2.9e-4
                && FlightPathUtils.isPointAlongArc(fromVectorPath, fromVectorMidVec, Math.PI, intersection)
                && !FlightPathUtils.isPointAlongArc(toVectorPath, toVectorMidVec, Math.PI, intersection));
        }
        /**
         * Calculates a leg-to-leg anticipated turn transition between a great-circle ("track") vector and a small-circle
         * ("turn") vector.
         * @param legs An array containing the legs for which to calculate transitions.
         * @param fromIndex The index of the transition's FROM leg.
         * @param toIndex The index of the transition's TO leg.
         * @param endIndex The index of the flight plan leg at which to stop calculating transitions. The last transition to
         * be calculated will be between the this leg and the previous leg.
         * @param state The airplane state to use for calculations.
         * @param fromVector The last base flight path vector of the transition's FROM leg (the FROM vector).
         * @param toVector The first base flight path vector of the transition's TO leg (the TO vector).
         * @param isTurnFirst Whether the FROM vector is the turn vector.
         * @param fromVectorPath A GeoCircle that defines the path of the FROM vector.
         * @param toVectorPath A GeoCircle that defines the path of the TO vector.
         * @param turnCircle A GeoCircle whose center and radius equal the center and radius of the turn described by the
         * turn vector.
         * @param trackPath A GeoCircle that defines the path of the track vector.
         * @param intersection The intersection point between the GeoCircles defining the FROM and TO vector paths to use as
         * the effective point at which the FROM and TO vectors are joined.
         * @param intersectionFromVectorEndOffset The along-vector offset distance, in great-arc radians, of the intersection
         * from the end point of the FROM vector. Positive offsets indicate the intersection is located after the end point.
         * @param intersectionToVectorStartOffset The along-vector offset distance, in great-arc radians, of the intersection
         * from the start point of the TO vector. Positive offsets indicate the intersection is located before the start
         * point.
         * @param isReversal Whether the anticipated turn is a 180-degree turn.
         * @param isTransitionInsideTurn Whether the anticipated turn transition should be positioned inside the GeoCircle
         * that defines the turn vector.
         * @param transitionTurnRadius The radius of the anticipated turn, in great-arc radians.
         * @param transitionTurnDirection The direction of the anticipated turn.
         * @param turnCircleToTransitionTurnCenterOffsetSign The sign of the distance offset from the `turnCircle` GeoCircle
         * to the center of the anticipated turn (-1 or 1). A positive offset sign indicates that `turnCircle` does not
         * encircle the center of the anticipated turn.
         * @param trackPathToTransitionTurnCenterOffsetSign The sign of the distance offset from the `trackPath` GeoCircle
         * to the center of the anticipated turn (-1 or 1). A positive offset sign indicates that `trackPath` does not
         * encircle the center of the anticipated turn.
         * @returns The number of consecutive leg-to-leg transitions calculated by this method.
         */
        calculateTrackTurnAnticipatedTurn(legs, fromIndex, toIndex, endIndex, state, fromVector, toVector, isTurnFirst, fromVectorPath, toVectorPath, turnCircle, trackPath, intersection, intersectionFromVectorEndOffset, intersectionToVectorStartOffset, isReversal, isTransitionInsideTurn, transitionTurnRadius, transitionTurnDirection, turnCircleToTransitionTurnCenterOffsetSign, trackPathToTransitionTurnCenterOffsetSign) {
            var _a, _b, _c;
            var _d, _e, _f;
            const fromLegCalc = legs[fromIndex].calculated;
            const toLegCalc = legs[toIndex].calculated;
            let transitionTurnStartVec;
            let transitionTurnEndVec;
            let transitionStartFromVectorEndOffset = 0;
            let transitionEndToVectorStartOffset = 0;
            let egressVectorCount = 0;
            let ingressVectorCount = 0;
            if (transitionTurnRadius <= GeoMath.ANGULAR_TOLERANCE) {
                transitionTurnStartVec = intersection;
                transitionTurnEndVec = intersection;
                transitionStartFromVectorEndOffset = intersectionFromVectorEndOffset;
                transitionEndToVectorStartOffset = intersectionToVectorStartOffset;
            }
            else {
                const turnCircleOffset = this.trackTurnCache.geoCircle[3].set(turnCircle.center, turnCircle.radius + transitionTurnRadius * turnCircleToTransitionTurnCenterOffsetSign);
                const trackPathOffset = this.trackTurnCache.geoCircle[4].set(trackPath.center, trackPath.radius + transitionTurnRadius * trackPathToTransitionTurnCenterOffsetSign);
                const intersections = this.trackTurnCache.intersection[0];
                const transitionTurnCenterCandidateCount = turnCircleOffset.intersection(trackPathOffset, intersections);
                if (transitionTurnCenterCandidateCount === 0) {
                    this.setEmptyTransition(fromLegCalc, toLegCalc);
                    return 1;
                }
                const transitionTurnCenterVec = transitionTurnCenterCandidateCount === 2
                    && (isReversal
                        || Vec3Math.unitAngle(intersection, intersections[0]) >= Vec3Math.unitAngle(intersection, intersections[1])) ? intersections[1] : intersections[0];
                transitionTurnStartVec = (isTurnFirst ? turnCircle : trackPath).closest(transitionTurnCenterVec, this.trackTurnCache.vec3[3]);
                transitionTurnEndVec = (isTurnFirst ? trackPath : turnCircle).closest(transitionTurnCenterVec, this.trackTurnCache.vec3[4]);
                const transitionTurnCircle = FlightPathUtils.getTurnCircle(transitionTurnCenterVec, transitionTurnRadius, transitionTurnDirection, this.trackTurnCache.geoCircle[3]);
                const transitionTurnMiddleVec = transitionTurnCircle.offsetAngleAlong(transitionTurnStartVec, transitionTurnCircle.angleAlong(transitionTurnStartVec, transitionTurnEndVec, Math.PI, GeoMath.ANGULAR_TOLERANCE) / 2, this.trackTurnCache.vec3[5], Math.PI);
                this.setAnticipatedTurn(fromLegCalc, toLegCalc, transitionTurnRadius, transitionTurnDirection, transitionTurnCenterVec, transitionTurnStartVec, transitionTurnMiddleVec, transitionTurnEndVec, false);
                const intersectionTransitionStartOffset = MathUtils.normalizeAngle(fromVectorPath.angleAlong(transitionTurnStartVec, intersection, Math.PI), -Math.PI);
                const intersectionTransitionEndOffset = MathUtils.normalizeAngle(toVectorPath.angleAlong(intersection, transitionTurnEndVec, Math.PI), -Math.PI);
                transitionStartFromVectorEndOffset = intersectionTransitionStartOffset - intersectionFromVectorEndOffset;
                transitionEndToVectorStartOffset = intersectionTransitionEndOffset - intersectionToVectorStartOffset;
                egressVectorCount = 1;
                ingressVectorCount = 1;
            }
            if (Math.abs(transitionStartFromVectorEndOffset) > 1e-5) {
                // The transition does not begin within ~60 meters of the end of the FROM vector. If the transition begins
                // after the end of the FROM vector, then we need to extend the transition at the egress end so that it joins
                // the FROM vector. If the transition begins before the end of the FROM vector and there is no anticipated turn
                // vector in the egress, then we need to add a zero-length egress vector so that we can properly mark the
                // beginning of the transition.
                if (transitionStartFromVectorEndOffset < 0 || egressVectorCount === 0) {
                    // Copy the first egress vector if it exists to the second egress vector.
                    if (egressVectorCount > 0) {
                        const movedVector = (_a = (_d = fromLegCalc.egress)[1]) !== null && _a !== void 0 ? _a : (_d[1] = FlightPathUtils.createEmptyVector());
                        Object.assign(movedVector, fromLegCalc.egress[0]);
                    }
                    const vectorEnd = this.trackTurnCache.geoPoint[0].setFromCartesian(transitionTurnStartVec);
                    const vectorStart = transitionStartFromVectorEndOffset < 0
                        ? this.trackTurnCache.geoPoint[1].set(fromVector.endLat, fromVector.endLon)
                        : vectorEnd;
                    const flags = exports.FlightPathVectorFlags.LegToLegTurn
                        | exports.FlightPathVectorFlags.AnticipatedTurn
                        | (fromVector.flags & exports.FlightPathVectorFlags.Fallback);
                    FlightPathUtils.setVectorFromCircle((_b = (_e = fromLegCalc.egress)[0]) !== null && _b !== void 0 ? _b : (_e[0] = FlightPathUtils.createEmptyVector()), fromVectorPath, vectorStart, vectorEnd, flags, fromVector.heading, fromVector.isHeadingTrue);
                    fromLegCalc.egressJoinIndex = fromLegCalc.flightPath.length - 1;
                    egressVectorCount += 1;
                }
            }
            if (Math.abs(transitionEndToVectorStartOffset) > 1e-5) {
                // The transition does not end within ~60 meters of the start of the TO vector. If the transition begins before
                // the start of the TO vector, then we need to extend the transition at the ingress end so that it joins the TO
                // TO vector. If the transition ends after the end of the TO vector and there is no anticipated turn vector in
                // the ingress, then we need to add a zero-length ingress vector so that we can properly mark the end of the
                // transition.
                if (transitionEndToVectorStartOffset < 0 || ingressVectorCount === 0) {
                    const vectorStart = this.trackTurnCache.geoPoint[0].setFromCartesian(transitionTurnEndVec);
                    const vectorEnd = transitionEndToVectorStartOffset < 0
                        ? this.trackTurnCache.geoPoint[1].set(toVector.startLat, toVector.startLon)
                        : vectorStart;
                    const flags = exports.FlightPathVectorFlags.LegToLegTurn
                        | exports.FlightPathVectorFlags.AnticipatedTurn
                        | (toVector.flags & exports.FlightPathVectorFlags.Fallback);
                    FlightPathUtils.setVectorFromCircle((_c = (_f = toLegCalc.ingress)[ingressVectorCount]) !== null && _c !== void 0 ? _c : (_f[ingressVectorCount] = FlightPathUtils.createEmptyVector()), toVectorPath, vectorStart, vectorEnd, flags, toVector.heading, toVector.isHeadingTrue);
                    toLegCalc.ingressJoinIndex = 0;
                    ingressVectorCount += 1;
                }
            }
            fromLegCalc.egress.length = egressVectorCount;
            if (egressVectorCount === 0) {
                fromLegCalc.egressJoinIndex = -1;
            }
            toLegCalc.ingress.length = ingressVectorCount;
            if (ingressVectorCount === 0) {
                toLegCalc.ingressJoinIndex = -1;
            }
            return 1;
        }
        /**
         * Checks whether the contents of a transition vector array can be replaced with calculated leg-to-leg transition
         * vectors.
         * @param transition The transition vector array to check.
         * @returns Whether the contents of the specified transition vector array can be replaced with calculated leg-to-leg
         * transition vectors.
         */
        static canCalculateTransition(transition) {
            return transition.length === 0 || BitFlags.isAll(transition[0].flags, exports.FlightPathVectorFlags.LegToLegTurn);
        }
        /**
         * Gets the along-circle offset distance from a reference point to a query point, in great-arc radians. The offset
         * is signed, with positive values indicating offsets in the direction of the circle. The calculated offset has the
         * range `[-c / 2, c / 2)`, where `c` is the circumference of the circle.
         * @param circle The geo circle along which to measure the offset.
         * @param reference The reference point.
         * @param query The query point.
         * @param equalityTolerance The tolerance for considering the reference and query points to be equal, in great-arc
         * radians. If the absolute (direction-agnostic) along-circle distance between the reference and query points is less
         * than or equal to this value, then zero will be returned. Defaults to `0`.
         * @returns The along-circle offset distance from the specified reference point to the query point, in great-arc
         * radians.
         */
        static getAlongCircleOffset(circle, reference, query, equalityTolerance) {
            return circle.arcLength(MathUtils.normalizeAngle(circle.angleAlong(reference, query, Math.PI, equalityTolerance), -Math.PI));
        }
    }

    /**
     * Modes for calculating airplane speed for use in flight path calculations.
     */
    exports.FlightPathAirplaneSpeedMode = void 0;
    (function (FlightPathAirplaneSpeedMode) {
        /** The default airplane speed is always used. */
        FlightPathAirplaneSpeedMode["Default"] = "Default";
        /** Ground speed is used. */
        FlightPathAirplaneSpeedMode["GroundSpeed"] = "GroundSpeed";
        /** True airspeed is used. */
        FlightPathAirplaneSpeedMode["TrueAirspeed"] = "TrueAirspeed";
        /** True airspeed plus wind speed is used. */
        FlightPathAirplaneSpeedMode["TrueAirspeedPlusWind"] = "TrueAirspeedPlusWind";
        /** A custom-defined speed is used. **Not yet supported.** */
        FlightPathAirplaneSpeedMode["Custom"] = "Custom";
    })(exports.FlightPathAirplaneSpeedMode || (exports.FlightPathAirplaneSpeedMode = {}));
    /**
     * Modes for obtaining wind data for use in flight path calculations.
     */
    exports.FlightPathAirplaneWindMode = void 0;
    (function (FlightPathAirplaneWindMode) {
        /** No wind data are used. */
        FlightPathAirplaneWindMode["None"] = "None";
        /** Wind data from the sim are used. */
        FlightPathAirplaneWindMode["Automatic"] = "Automatic";
        /** Custom-defined wind data are used. **Not yet supported.** */
        FlightPathAirplaneWindMode["Custom"] = "Custom";
    })(exports.FlightPathAirplaneWindMode || (exports.FlightPathAirplaneWindMode = {}));
    /**
     * Calculates the flight path vectors for a given set of legs.
     */
    class FlightPathCalculator {
        /**
         * Creates an instance of the FlightPathCalculator.
         * @param facilityClient The facility loader to use with this instance.
         * @param options The options to use with this flight path calculator.
         * @param bus An instance of the EventBus.
         */
        constructor(facilityClient, options, bus) {
            var _a, _b, _c;
            this.facilityClient = facilityClient;
            this.bus = bus;
            this.facilityCache = new Map();
            this.legCalculatorMap = this.createLegCalculatorMap();
            this.turnCalculator = new FlightPathLegToLegCalculator();
            this.state = new FlightPathStateClass();
            this.legCalcOptions = {
                calculateDiscontinuityVectors: false
            };
            this.calculateQueue = [];
            this.isBusy = false;
            this.id = (_a = options === null || options === void 0 ? void 0 : options.id) !== null && _a !== void 0 ? _a : '';
            this.options = {
                defaultClimbRate: 0,
                defaultSpeed: 0,
                bankAngle: 0,
                holdBankAngle: null,
                courseReversalBankAngle: null,
                turnAnticipationBankAngle: null,
                maxBankAngle: 0,
                airplaneSpeedMode: exports.FlightPathAirplaneSpeedMode.Default,
                airplaneWindMode: exports.FlightPathAirplaneWindMode.None
            };
            this.setOptions(options);
            this.staticOptions = {
                calculateDiscontinuityVectors: (_b = options.calculateDiscontinuityVectors) !== null && _b !== void 0 ? _b : false
            };
            this.legCalcOptions.calculateDiscontinuityVectors = this.staticOptions.calculateDiscontinuityVectors;
            this.initSyncSubscriptions((_c = options.initSyncRole) !== null && _c !== void 0 ? _c : 'none');
        }
        /**
         * Initializes this calculator's sync subscriptions.
         * @param initialSyncRole This calculator's initial sync role.
         */
        initSyncSubscriptions(initialSyncRole) {
            const eventBusTopicSuffix = this.id === '' ? '' : `_${this.id}`;
            const publisher = this.bus.getPublisher();
            const sub = this.bus.getSubscriber();
            if (initialSyncRole === 'primary') {
                publisher.pub(`flightpath_sync_init${eventBusTopicSuffix}`, { options: this.options, staticOptions: this.staticOptions }, true, false);
                sub.on(`flightpath_sync_options_request${eventBusTopicSuffix}`).handle(uid => {
                    publisher.pub(`flightpath_sync_options_response${eventBusTopicSuffix}`, { uid, options: this.options, staticOptions: this.staticOptions }, true, false);
                });
            }
            else if (initialSyncRole === 'replica') {
                const requestUid = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
                sub.on(`flightpath_sync_options_response${eventBusTopicSuffix}`).handle(data => {
                    if (data.uid === requestUid) {
                        this.setStaticOptions(data.staticOptions);
                        this.setOptions(data.options);
                    }
                });
                publisher.pub(`flightpath_sync_options_request${eventBusTopicSuffix}`, requestUid, true, false);
                sub.on(`flightpath_sync_init${eventBusTopicSuffix}`).handle(data => {
                    this.setStaticOptions(data.staticOptions);
                    this.setOptions(data.options);
                });
            }
            sub.on(`flightpath_set_options${eventBusTopicSuffix}`).handle(this.setOptions.bind(this));
        }
        /**
         * Sets this calculator's static options.
         * @param options The options to set.
         */
        setStaticOptions(options) {
            this.staticOptions.calculateDiscontinuityVectors = options.calculateDiscontinuityVectors;
        }
        /**
         * Sets this calculator's options.
         * @param newOptions The new options to set.
         */
        setOptions(newOptions) {
            for (const key in this.options) {
                const option = newOptions[key];
                if (option !== undefined) {
                    switch (key) {
                        case 'bankAngle':
                            this.options.bankAngle = this.processBankAngleOption(option);
                            this.bankAngleTable = this.buildBankAngleTable(this.options.bankAngle);
                            break;
                        case 'holdBankAngle':
                            this.options.holdBankAngle = this.processBankAngleOption(option);
                            this.holdBankAngleTable = this.options.holdBankAngle === null ? undefined : this.buildBankAngleTable(this.options.holdBankAngle);
                            break;
                        case 'courseReversalBankAngle':
                            this.options.courseReversalBankAngle = this.processBankAngleOption(option);
                            this.courseReversalBankAngleTable = this.options.courseReversalBankAngle === null ? undefined : this.buildBankAngleTable(this.options.courseReversalBankAngle);
                            break;
                        case 'turnAnticipationBankAngle':
                            this.options.turnAnticipationBankAngle = this.processBankAngleOption(option);
                            this.turnAnticipationBankAngleTable = this.options.turnAnticipationBankAngle === null ? undefined : this.buildBankAngleTable(this.options.turnAnticipationBankAngle);
                            break;
                        default:
                            this.options[key] = option;
                    }
                }
            }
        }
        /**
         * Processes a bank angle option to a form that is safe to store in this calculator's options record.
         * @param option The option to process.
         * @returns The processed option.
         */
        processBankAngleOption(option) {
            if (Array.isArray(option)) {
                const copy = [];
                for (let i = 0; i < option.length; i++) {
                    copy.push(Array.from(option[i]));
                }
                return copy;
            }
            else {
                return option;
            }
        }
        /**
         * Builds a bank angle lookup table.
         * @param angle A constant bank angle, in degrees, or an array of bank angle (degrees) versus airplane speed (knots)
         * breakpoints.
         * @returns A bank angle lookup table.
         */
        buildBankAngleTable(angle) {
            if (typeof angle === 'number') {
                return new LerpLookupTable([[angle, 0]]);
            }
            else {
                return new LerpLookupTable(angle);
            }
        }
        /**
         * Creates a map from leg types to leg calculators.
         * @returns A map from leg types to leg calculators.
         */
        createLegCalculatorMap() {
            let calc;
            return {
                [exports.LegType.IF]: calc = new TrackToFixLegCalculator(this.facilityCache),
                [exports.LegType.TF]: calc,
                [exports.LegType.AF]: new ArcToFixLegCalculator(this.facilityCache),
                [exports.LegType.CD]: new DmeInterceptLegCalculator(this.facilityCache, false),
                [exports.LegType.VD]: new DmeInterceptLegCalculator(this.facilityCache, true),
                [exports.LegType.CF]: new CourseToFixLegCalculator(this.facilityCache),
                [exports.LegType.CR]: new RadialInterceptLegCalculator(this.facilityCache, false),
                [exports.LegType.VR]: new RadialInterceptLegCalculator(this.facilityCache, true),
                [exports.LegType.FC]: new TrackFromFixLegCalculator(this.facilityCache),
                [exports.LegType.FD]: new FixToDmeLegCalculator(this.facilityCache),
                [exports.LegType.RF]: new RadiusToFixLegCalculator(this.facilityCache),
                [exports.LegType.DF]: new DirectToFixLegCalculator(this.facilityCache),
                [exports.LegType.FA]: new FixToAltitudeLegCalculator(this.facilityCache),
                [exports.LegType.CA]: new CourseToAltitudeLegCalculator(this.facilityCache),
                [exports.LegType.VA]: new HeadingToAltitudeLegCalculator(this.facilityCache),
                [exports.LegType.FM]: new FixToManualLegCalculator(this.facilityCache),
                [exports.LegType.VM]: new HeadingToManualLegCalculator(this.facilityCache),
                [exports.LegType.CI]: new LegInterceptLegCalculator(this.facilityCache, false),
                [exports.LegType.VI]: new LegInterceptLegCalculator(this.facilityCache, true),
                [exports.LegType.PI]: new ProcedureTurnLegCalculator(this.facilityCache),
                [exports.LegType.HA]: calc = new HoldLegCalculator(this.facilityCache),
                [exports.LegType.HM]: calc,
                [exports.LegType.HF]: calc,
                [exports.LegType.Discontinuity]: calc = new DiscontinuityLegCalculator(this.facilityCache),
                [exports.LegType.ThruDiscontinuity]: calc,
                [exports.LegType.Unknown]: new NoPathLegCalculator(this.facilityCache),
            };
        }
        /**
         * Calculates a flight path for a given set of flight plan legs.
         * @param legs The legs of the flight plan to calculate.
         * @param activeLegIndex The index of the active leg.
         * @param initialIndex The index of the leg at which to start the calculation.
         * @param count The number of legs to calculate.
         * @returns A Promise which is fulfilled when the calculation is finished.
         */
        calculateFlightPath(legs, activeLegIndex, initialIndex = 0, count = Number.POSITIVE_INFINITY) {
            if (this.isBusy || this.calculateQueue.length > 0) {
                return new Promise((resolve, reject) => {
                    this.calculateQueue.push(() => { this.doCalculate(resolve, reject, legs, activeLegIndex, initialIndex, count); });
                });
            }
            else {
                return new Promise((resolve, reject) => {
                    this.doCalculate(resolve, reject, legs, activeLegIndex, initialIndex, count);
                });
            }
        }
        /**
         * Executes a calculate operation. When the operation is finished, the next operation in the queue, if one exists,
         * will be started.
         * @param resolve The Promise resolve function to invoke when the calculation is finished.
         * @param reject The Promise reject function to invoke when an error occurs during calculation.
         * @param legs The legs of the flight plan to calculate.
         * @param activeLegIndex The index of the active leg.
         * @param initialIndex The index of the leg at which to start the calculation.
         * @param count The number of legs to calculate.
         * @returns A Promise which is fulfilled when the calculate operation is finished, or rejected if an error occurs
         * during calculation.
         */
        async doCalculate(resolve, reject, legs, activeLegIndex, initialIndex = 0, count = Number.POSITIVE_INFINITY) {
            this.isBusy = true;
            try {
                initialIndex = Math.max(0, initialIndex);
                count = Math.max(0, Math.min(legs.length - initialIndex, count));
                this.state.updatePlaneState(this.options, this.bankAngleTable, this.holdBankAngleTable, this.courseReversalBankAngleTable, this.turnAnticipationBankAngleTable);
                // Because some facilities can be mutated, we always want to get the most up-to-date version from the facility loader
                this.facilityCache.clear();
                await this.loadFacilities(legs, initialIndex, count);
                this.initState(legs, initialIndex);
                this.calculateLegPaths(legs, activeLegIndex, initialIndex, count);
                this.turnCalculator.calculate(legs, initialIndex, count, this.state);
                this.finishLegCalculations(legs, initialIndex, count);
                this.isBusy = false;
                resolve();
            }
            catch (e) {
                this.isBusy = false;
                reject(e);
            }
            const nextInQueue = this.calculateQueue.shift();
            if (nextInQueue !== undefined) {
                nextInQueue();
            }
        }
        /**
         * Loads facilities required for flight path calculations from the flight plan.
         * @param legs The legs of the flight plan to calculate.
         * @param initialIndex The index of the first leg to calculate.
         * @param count The number of legs to calculate.
         */
        async loadFacilities(legs, initialIndex, count) {
            const facilitiesToLoad = [];
            for (let i = initialIndex; i < initialIndex + count; i++) {
                const leg = legs[i].leg;
                ICAO.isValueFacility(leg.fixIcaoStruct) && facilitiesToLoad.push(leg.fixIcaoStruct);
                ICAO.isValueFacility(leg.originIcaoStruct) && facilitiesToLoad.push(leg.originIcaoStruct);
                ICAO.isValueFacility(leg.arcCenterFixIcaoStruct) && facilitiesToLoad.push(leg.arcCenterFixIcaoStruct);
            }
            if (facilitiesToLoad.length > 0) {
                const facs = await this.facilityClient.getFacilities(facilitiesToLoad, 0);
                for (const loadedFac of facs) {
                    if (!loadedFac) {
                        continue;
                    }
                    // TODO we should refactor facilityCache to use `ICAO.getUid()` as the key
                    this.facilityCache.set(loadedFac.icao, loadedFac);
                }
                //PM Modif
                // We must create unexisting user falilities
                // This case may happen with the toolbar panel and external WT mode when the initial Fp has user waypoints
                // Because the panel is loaded after the primary instrument so some events about user WP facility repos ar enot sent (ex G1000Nxi).
                let legIndex = initialIndex;
                for (let facIndex = 0; facIndex < facs.length; facIndex++) {
                    if(!facs[facIndex]) {
                        let leg = legs[legIndex];
                        if(leg.calculated && leg.calculated.endLat != undefined && leg.calculated.endLon != undefined) {
                            const userFacility = msfssdk.UserFacilityUtils.createFromLatLon(msfssdk.ICAO.valueToStringV1(leg.leg.fixIcaoStruct), leg.calculated.endLat, leg.calculated.endLon);
                            if(userFacility) {
                                this.facilityClient.facilityRepo.add(userFacility);
                                gotUserFacilitiesAdded  =true;
                            }
                        }
                    }
                    legIndex++;
                }
            }
        }
        /**
         * Initializes the current flight path state.
         * @param legs The legs of the flight plan to calculate.
         * @param initialIndex The index of the first leg to calculate.
         */
        initState(legs, initialIndex) {
            var _a, _b, _c;
            this.state.currentPosition.set(NaN, NaN);
            this.state.currentCourse = undefined;
            let index = Math.min(initialIndex, legs.length);
            this.state.isFallback = (_c = (_b = (_a = legs[index - 1]) === null || _a === void 0 ? void 0 : _a.calculated) === null || _b === void 0 ? void 0 : _b.endsInFallback) !== null && _c !== void 0 ? _c : false;
            let isDiscontinuity;
            while (--index >= 0
                && (!this.state.currentPosition.isValid() || this.state.currentCourse === undefined)) {
                const leg = legs[index];
                const legCalc = leg.calculated;
                if (legCalc) {
                    isDiscontinuity !== null && isDiscontinuity !== void 0 ? isDiscontinuity : (isDiscontinuity = legCalc.endsInDiscontinuity);
                    if (!this.state.currentPosition.isValid() && legCalc && legCalc.endLat !== undefined && legCalc.endLon !== undefined) {
                        this.state.currentPosition.set(legCalc.endLat, legCalc.endLon);
                    }
                    if (this.state.currentCourse === undefined && legCalc && legCalc.flightPath.length > 0) {
                        this.state.currentCourse = FlightPathUtils.getLegFinalCourse(legCalc);
                    }
                }
                else {
                    isDiscontinuity !== null && isDiscontinuity !== void 0 ? isDiscontinuity : (isDiscontinuity = FlightPlanUtils.isDiscontinuityLeg(leg.leg.type) || FlightPlanUtils.isManualDiscontinuityLeg(leg.leg.type));
                }
            }
            this.state.isDiscontinuity = isDiscontinuity !== null && isDiscontinuity !== void 0 ? isDiscontinuity : false;
        }
        /**
         * Calculates flight paths for a sequence of flight plan legs.
         * @param legs A sequence of flight plan legs.
         * @param activeLegIndex The index of the active leg.
         * @param initialIndex The index of the first leg to calculate.
         * @param count The number of legs to calculate.
         */
        calculateLegPaths(legs, activeLegIndex, initialIndex, count) {
            const end = initialIndex + count;
            for (let calculateIndex = initialIndex; calculateIndex < end; calculateIndex++) {
                const definition = legs[calculateIndex];
                this.legCalculatorMap[definition.leg.type].calculate(legs, calculateIndex, activeLegIndex, this.state, this.legCalcOptions);
            }
        }
        /**
         * Resolves the ingress to egress vectors and calculates leg distances for a set of flight plan legs.
         * @param legs A sequence of flight plan legs.
         * @param initialIndex The index of the first leg to calculate.
         * @param count The number of legs to calculate.
         */
        finishLegCalculations(legs, initialIndex, count) {
            const end = initialIndex + count;
            let prevLegCalc;
            for (let i = 0; i < end; i++) {
                const legCalc = legs[i].calculated;
                if (!legCalc) {
                    continue;
                }
                if (i >= initialIndex) {
                    FlightPathUtils.resolveIngressToEgress(legCalc);
                    // Calculate distance without transitions.
                    legCalc.distance = 0;
                    const len = legCalc.flightPath.length;
                    for (let j = 0; j < len; j++) {
                        legCalc.distance += legCalc.flightPath[j].distance;
                    }
                    // Calculate distance with transitions.
                    legCalc.distanceWithTransitions = 0;
                    const ingressLen = legCalc.ingress.length;
                    for (let j = 0; j < ingressLen; j++) {
                        legCalc.distanceWithTransitions += legCalc.ingress[j].distance;
                    }
                    const ingressToEgressLen = legCalc.ingressToEgress.length;
                    for (let j = 0; j < ingressToEgressLen; j++) {
                        legCalc.distanceWithTransitions += legCalc.ingressToEgress[j].distance;
                    }
                    const egressLen = legCalc.egress.length;
                    for (let j = 0; j < egressLen; j++) {
                        legCalc.distanceWithTransitions += legCalc.egress[j].distance;
                    }
                    // Calculate cumulative distances.
                    legCalc.cumulativeDistance = legCalc.distance;
                    legCalc.cumulativeDistanceWithTransitions = legCalc.distanceWithTransitions;
                    if (prevLegCalc) {
                        legCalc.cumulativeDistance += prevLegCalc.cumulativeDistance;
                        legCalc.cumulativeDistanceWithTransitions += prevLegCalc.cumulativeDistanceWithTransitions;
                    }
                }
                prevLegCalc = legCalc;
            }
        }
    }
    /**
     * An implementation of {@link FlightPathState}
     */
    class FlightPathStateClass {
        constructor() {
            /** @inheritDoc */
            this.currentPosition = new GeoPoint(NaN, NaN);
            /** @inheritDoc */
            this.isDiscontinuity = false;
            /** @inheritDoc */
            this.isFallback = false;
            this._planePosition = new GeoPoint(0, 0);
            /** @inheritDoc */
            this.planePosition = this._planePosition.readonly;
            this._planeHeading = 0;
            this._planeAltitude = UnitType.FOOT.createNumber(0);
            /** @inheritDoc */
            this.planeAltitude = this._planeAltitude.readonly;
            this._planeSpeed = UnitType.KNOT.createNumber(0);
            /** @inheritDoc */
            this.planeSpeed = this._planeSpeed.readonly;
            this._planeClimbRate = UnitType.FPM.createNumber(0);
            /** @inheritDoc */
            this.planeClimbRate = this._planeClimbRate.readonly;
            this._planeTrueAirspeed = UnitType.KNOT.createNumber(0);
            /** @inheritDoc */
            this.planeTrueAirspeed = this._planeTrueAirspeed.readonly;
            this._planeWindDirection = 0;
            this._planeWindSpeed = UnitType.KNOT.createNumber(0);
            /** @inheritDoc */
            this.planeWindSpeed = this._planeWindSpeed.readonly;
            this._desiredTurnRadius = UnitType.METER.createNumber(0);
            /** @inheritDoc */
            this.desiredTurnRadius = this._desiredTurnRadius.readonly;
            this._desiredHoldTurnRadius = UnitType.METER.createNumber(0);
            /** @inheritDoc */
            this.desiredHoldTurnRadius = this._desiredHoldTurnRadius.readonly;
            this._desiredCourseReversalTurnRadius = UnitType.METER.createNumber(0);
            /** @inheritDoc */
            this.desiredCourseReversalTurnRadius = this._desiredCourseReversalTurnRadius.readonly;
            this._desiredTurnAnticipationTurnRadius = UnitType.METER.createNumber(0);
            /** @inheritDoc */
            this.desiredTurnAnticipationTurnRadius = this._desiredTurnAnticipationTurnRadius.readonly;
        }
        /** @inheritDoc */
        get planeHeading() {
            return this._planeHeading;
        }
        /** @inheritDoc */
        get planeWindDirection() {
            return this._planeWindDirection;
        }
        /**
         * Updates this state with the latest information on the airplane.
         * @param options Flight path calculator options.
         * @param bankAngleTable A lookup table for general turn bank angle, in degrees, versus airplane speed.
         * @param holdBankAngleTable A lookup table for hold turn bank angle, in degrees, versus airplane speed, in knots.
         * If not defined, the general turn bank angle table will be used instead.
         * @param courseReversalBankAngleTable A lookup table for course reversal turn bank angle, in degrees, versus
         * airplane speed, in knots. If not defined, the general turn bank angle table will be used instead.
         * @param turnAnticipationBankAngleTable A lookup table for turn anticipation bank angle, in degrees, versus airplane
         * speed, in knots. If not defined, the general turn bank angle table will be used instead.
         */
        updatePlaneState(options, bankAngleTable, holdBankAngleTable, courseReversalBankAngleTable, turnAnticipationBankAngleTable) {
            this._planePosition.set(SimVar.GetSimVarValue('PLANE LATITUDE', exports.SimVarValueType.Degree), SimVar.GetSimVarValue('PLANE LONGITUDE', exports.SimVarValueType.Degree));
            this._planeAltitude.set(SimVar.GetSimVarValue('INDICATED ALTITUDE', 'feet'));
            this._planeHeading = SimVar.GetSimVarValue('PLANE HEADING DEGREES TRUE', 'degree');
            const tas = SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.Knots);
            this._planeTrueAirspeed.set(Math.max(tas, options.defaultSpeed));
            switch (options.airplaneWindMode) {
                case exports.FlightPathAirplaneWindMode.Automatic:
                    this._planeWindDirection = SimVar.GetSimVarValue('AMBIENT WIND DIRECTION', exports.SimVarValueType.Degree);
                    this._planeWindSpeed.set(SimVar.GetSimVarValue('AMBIENT WIND VELOCITY', exports.SimVarValueType.Knots));
                    break;
                default:
                    this._planeWindDirection = 0;
                    this._planeWindSpeed.set(0);
            }
            switch (options.airplaneSpeedMode) {
                case exports.FlightPathAirplaneSpeedMode.GroundSpeed:
                    this._planeSpeed.set(Math.max(SimVar.GetSimVarValue('GROUND VELOCITY', exports.SimVarValueType.Knots), options.defaultSpeed));
                    break;
                case exports.FlightPathAirplaneSpeedMode.TrueAirspeed:
                case exports.FlightPathAirplaneSpeedMode.TrueAirspeedPlusWind: {
                    const windSpeed = options.airplaneSpeedMode === exports.FlightPathAirplaneSpeedMode.TrueAirspeedPlusWind
                        ? this._planeWindSpeed.number
                        : 0;
                    this._planeSpeed.set(Math.max(tas + windSpeed, options.defaultSpeed));
                    break;
                }
                default:
                    this._planeSpeed.set(options.defaultSpeed);
            }
            this._planeClimbRate.set(Math.max(SimVar.GetSimVarValue('VERTICAL SPEED', 'feet per minute'), options.defaultClimbRate));
            const planeSpeedKnots = this._planeSpeed.asUnit(UnitType.KNOT);
            this._desiredTurnRadius.set(NavMath.turnRadius(planeSpeedKnots, Math.min(bankAngleTable.get(planeSpeedKnots), options.maxBankAngle)));
            if (holdBankAngleTable) {
                this._desiredHoldTurnRadius.set(NavMath.turnRadius(planeSpeedKnots, Math.min(holdBankAngleTable.get(planeSpeedKnots), options.maxBankAngle)));
            }
            else {
                this._desiredHoldTurnRadius.set(this._desiredTurnRadius);
            }
            if (courseReversalBankAngleTable) {
                this._desiredCourseReversalTurnRadius.set(NavMath.turnRadius(planeSpeedKnots, Math.min(courseReversalBankAngleTable.get(planeSpeedKnots), options.maxBankAngle)));
            }
            else {
                this._desiredCourseReversalTurnRadius.set(this._desiredTurnRadius);
            }
            if (turnAnticipationBankAngleTable) {
                this._desiredTurnAnticipationTurnRadius.set(NavMath.turnRadius(planeSpeedKnots, Math.min(turnAnticipationBankAngleTable.get(planeSpeedKnots), options.maxBankAngle)));
            }
            else {
                this._desiredTurnAnticipationTurnRadius.set(this._desiredTurnRadius);
            }
        }
    }

    /**
     * A prototype for signalling application-specific type metadata for plan segments.
     */
    exports.FlightPlanSegmentType = void 0;
    (function (FlightPlanSegmentType) {
        FlightPlanSegmentType["Origin"] = "Origin";
        FlightPlanSegmentType["Departure"] = "Departure";
        FlightPlanSegmentType["Enroute"] = "Enroute";
        FlightPlanSegmentType["Arrival"] = "Arrival";
        FlightPlanSegmentType["Approach"] = "Approach";
        FlightPlanSegmentType["Destination"] = "Destination";
        FlightPlanSegmentType["MissedApproach"] = "MissedApproach";
        FlightPlanSegmentType["RandomDirectTo"] = "RandomDirectTo";
    })(exports.FlightPlanSegmentType || (exports.FlightPlanSegmentType = {}));
    /**
     * A segment of a flight plan.
     */
    class FlightPlanSegment {
        /**
         * Creates a new FlightPlanSegment.
         * @param segmentIndex The index of the segment within the flight plan.
         * @param offset The leg offset within the original flight plan that
         * the segment starts at.
         * @param legs The legs in the flight plan segment.
         * @param segmentType The type of segment this is.
         * @param airway The airway associated with this segment, if any.
         */
        constructor(segmentIndex, offset, legs, segmentType = exports.FlightPlanSegmentType.Enroute, airway) {
            this.segmentIndex = segmentIndex;
            this.offset = offset;
            this.legs = legs;
            this.segmentType = segmentType;
            this.airway = airway;
        }
    }
    /** An empty flight plan segment. */
    FlightPlanSegment.Empty = new FlightPlanSegment(-1, -1, []);
    /**
     * Bitflags describing a leg definition.
     */
    exports.LegDefinitionFlags = void 0;
    (function (LegDefinitionFlags) {
        LegDefinitionFlags[LegDefinitionFlags["None"] = 0] = "None";
        LegDefinitionFlags[LegDefinitionFlags["DirectTo"] = 1] = "DirectTo";
        LegDefinitionFlags[LegDefinitionFlags["MissedApproach"] = 2] = "MissedApproach";
        LegDefinitionFlags[LegDefinitionFlags["Obs"] = 4] = "Obs";
        LegDefinitionFlags[LegDefinitionFlags["VectorsToFinal"] = 8] = "VectorsToFinal";
        LegDefinitionFlags[LegDefinitionFlags["VectorsToFinalFaf"] = 16] = "VectorsToFinalFaf";
    })(exports.LegDefinitionFlags || (exports.LegDefinitionFlags = {}));
    /**
     * Vertical flight phase.
     */
    exports.VerticalFlightPhase = void 0;
    (function (VerticalFlightPhase) {
        VerticalFlightPhase["Climb"] = "Climb";
        VerticalFlightPhase["Descent"] = "Descent";
    })(exports.VerticalFlightPhase || (exports.VerticalFlightPhase = {}));
    exports.SpeedUnit = void 0;
    (function (SpeedUnit) {
        SpeedUnit[SpeedUnit["IAS"] = 0] = "IAS";
        SpeedUnit[SpeedUnit["MACH"] = 1] = "MACH";
    })(exports.SpeedUnit || (exports.SpeedUnit = {}));

    exports.LegEventType = void 0;
    (function (LegEventType) {
        LegEventType["Added"] = "Added";
        LegEventType["Removed"] = "Removed";
        LegEventType["Changed"] = "Changed";
    })(exports.LegEventType || (exports.LegEventType = {}));
    exports.SegmentEventType = void 0;
    (function (SegmentEventType) {
        SegmentEventType["Added"] = "Added";
        SegmentEventType["Removed"] = "Removed";
        SegmentEventType["Changed"] = "Changed";
        SegmentEventType["Inserted"] = "Inserted";
    })(exports.SegmentEventType || (exports.SegmentEventType = {}));
    exports.ActiveLegType = void 0;
    (function (ActiveLegType) {
        ActiveLegType["Lateral"] = "Lateral";
        ActiveLegType["Vertical"] = "Vertical";
        ActiveLegType["Calculating"] = "Calculating";
    })(exports.ActiveLegType || (exports.ActiveLegType = {}));
    exports.OriginDestChangeType = void 0;
    (function (OriginDestChangeType) {
        OriginDestChangeType["OriginAdded"] = "OriginAdded";
        OriginDestChangeType["OriginRemoved"] = "OriginRemoved";
        OriginDestChangeType["DestinationAdded"] = "DestinationAdded";
        OriginDestChangeType["DestinationRemoved"] = "DestinationRemoved";
    })(exports.OriginDestChangeType || (exports.OriginDestChangeType = {}));
    /**
     * A flight plan managed by the flight plan system.
     */
    class FlightPlan {
        // eslint-disable-next-line jsdoc/require-returns
        /** The ICAO value of the flight plan's origin airport, or `undefined` if the flight plan has no origin airport. */
        get originAirportIcao() {
            return this._originAirportIcao;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The ICAO value of the flight plan's destination airport, or `undefined` if the flight plan has no destination
         * airport.
         */
        get destinationAirportIcao() {
            return this._destinationAirportIcao;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The ICAO string (V1) of the flight plan's origin airport, or `undefined` if the flight plan has no origin airport.
         * @deprecated Please use `originAirportIcao` instead.
         */
        get originAirport() {
            return this._originAirport;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The ICAO string (V1) of the flight plan's destination airport, or `undefined` if the flight plan has no
         * destination airport.
         * @deprecated Please use `destinationAirportIcao` instead.
         */
        get destinationAirport() {
            return this._destinationAirport;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The global index of the currently active lateral navigation leg. */
        get activeLateralLeg() {
            return this._activeLateralLeg;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The global index of the currently active vertical navigation leg. */
        get activeVerticalLeg() {
            return this._activeVerticalLeg;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The global index of the currently active calculating leg. */
        get activeCalculatingLeg() {
            return this._activeCalculatingLeg;
        }
        /**
         * Gets the current number of legs in the flight plan.
         * @returns The number of legs in the plan.
         */
        get length() {
            const segment = this.planSegments[this.planSegments.length - 1];
            if (segment !== undefined) {
                return segment.offset + segment.legs.length;
            }
            return 0;
        }
        /**
         * Gets the current number of segments in the flight plan.
         * @returns The number of legs in the plan.
         */
        get segmentCount() {
            return this.planSegments.length;
        }
        /**
         * Creates an instance of a FlightPlan.
         * @param planIndex The index within the flight planner of this flight plan.
         * @param calculator The flight path calculator to use to calculate the flight path.
         * @param onLegNameRequested A callback fired when a flight plan leg is to be named.
         */
        constructor(planIndex, calculator, onLegNameRequested) {
            this.planIndex = planIndex;
            this.calculator = calculator;
            this.onLegNameRequested = onLegNameRequested;
            this._activeLateralLeg = 0;
            this._activeVerticalLeg = 0;
            this._activeCalculatingLeg = 0;
            /** The direct to metadata for this plan. */
            this.directToData = { segmentIndex: -1, segmentLegIndex: -1 };
            /** Events fired when the plan is modified. */
            this.events = {};
            /** The details about the selected procedures. */
            this.procedureDetails = FlightPlan.createProcedureDetails();
            /** The flight plan segments that make up this flight plan. */
            this.planSegments = [];
            /** User assignable data. */
            this.userData = {};
            this.batchEntryStack = [];
            this.batchToCloseIndex = undefined;
            this.legIndexesCache = { segmentIndex: -1, segmentLegIndex: -1 };
        }
        /**
         * Gets this flight plan's legs.
         * @param reverse Whether to get the legs in reverse order. False by default.
         * @param startIndex The global leg index of the leg at which to start, inclusive. Defaults to `0` if `reverse` is
         * `false` or `this.length - 1` if `reverse` is `true`.
         * @param endIndex The global leg index of the leg at which to end, exclusive. Defaults to `this.length` if `reverse`
         * is `false` or `-1` if `reverse` is `true`.
         * @returns A generator which yields this flight plan's legs.
         */
        legs(reverse = false, startIndex, endIndex) {
            return reverse ? this._legsReverse(startIndex, endIndex) : this._legs(startIndex, endIndex);
        }
        /**
         * Gets this flight plan's legs in forward order.
         * @param startIndex The global leg index of the leg at which to start, inclusive. Defaults to `0`.
         * @param endIndex The global leg index of the leg at which to end, exclusive. Defaults to `this.length`.
         * @yields This flight plan's legs in forward order.
         */
        *_legs(startIndex = 0, endIndex = this.length) {
            endIndex = Math.min(this.length, endIndex);
            for (let i = 0; i < this.planSegments.length; i++) {
                const segment = this.planSegments[i];
                if (segment !== undefined && segment.legs.length > 0) {
                    const end = Math.min(segment.legs.length, endIndex - segment.offset);
                    if (end <= 0) {
                        return;
                    }
                    for (let l = Math.max(0, startIndex - segment.offset); l < end; l++) {
                        yield segment.legs[l];
                    }
                }
            }
        }
        /**
         * Gets this flight plan's legs in reverse order.
         * @param startIndex The global leg index of the leg at which to start, inclusive. Defaults to `this.length - 1`.
         * @param endIndex The global leg index of the leg at which to end, exclusive. Defaults to `-1`.
         * @yields This flight plan's legs in reverse order.
         */
        *_legsReverse(startIndex = this.length - 1, endIndex = -1) {
            endIndex = Math.max(-1, endIndex);
            for (let i = this.planSegments.length - 1; i > -1; i--) {
                const segment = this.planSegments[i];
                if (segment !== undefined && segment.legs.length > 0) {
                    const end = Math.max(-1, endIndex - segment.offset);
                    if (end >= segment.legs.length) {
                        return;
                    }
                    for (let l = Math.min(segment.legs.length - 1, startIndex - segment.offset); l > end; l--) {
                        yield segment.legs[l];
                    }
                }
            }
        }
        /**
         * Gets the segments for the flightplan.
         * @yields The flight plan segments.
         */
        *segments() {
            for (let i = 0; i < this.planSegments.length; i++) {
                const segment = this.planSegments[i];
                if (segment !== undefined) {
                    yield segment;
                }
            }
        }
        /**
         * Gets all of the segments of a given type.
         * @param segmentType The type of the segments to retrieve.
         * @yields The segments of the requested type.
         */
        *segmentsOfType(segmentType) {
            for (const segment of this.segments()) {
                if (segment.segmentType == segmentType) {
                    yield segment;
                }
            }
        }
        /**
         * Gets a flight plan segment from the plan.
         * @param segmentIndex The index of the segment to get; if not specified returns the active segment.
         * @returns The requested flight plan segment.
         * @throws An error if the flight plan segment could not be found.
         */
        getSegment(segmentIndex) {
            const segment = this.tryGetSegment(segmentIndex);
            if (segment === null) {
                throw new Error(`Flight plan segment with ${segmentIndex === undefined ? 'active leg' : `segment index ${segmentIndex}`} could not be found.`);
            }
            else {
                return segment;
            }
        }
        /**
         * Attempts to get a flight plan segment from the plan.
         * @param segmentIndex The index of the segment to get; if not specified returns the active segment.
         * @returns The requested flight plan segment, or `null` if it could not be found.
         */
        tryGetSegment(segmentIndex) {
            if (segmentIndex === undefined) {
                let calculatedSegmentIndex = 0;
                for (const segment of this.segments()) {
                    if (this.activeLateralLeg == 0 && segment.legs.length == 0) {
                        calculatedSegmentIndex++;
                    }
                    else if (this.activeLateralLeg > segment.offset + segment.legs.length) {
                        calculatedSegmentIndex++;
                    }
                    else {
                        break;
                    }
                }
                const segment = this.planSegments[calculatedSegmentIndex];
                if (segment !== undefined) {
                    return segment;
                }
            }
            else if (segmentIndex >= 0) {
                const segment = this.planSegments[segmentIndex];
                if (segment !== undefined) {
                    return segment;
                }
            }
            return null;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        getLeg(arg1, arg2) {
            const leg = this._tryGetLeg(arg1, arg2);
            if (leg) {
                return leg;
            }
            const indexes = typeof arg1 === 'object' || arg2 !== undefined
                ? arg2 === undefined
                    ? `segment index ${arg1.segmentIndex}, segment leg index ${arg1.segmentLegIndex}`
                    : `segment index ${arg1}, segment leg index ${arg2}`
                : `global leg index ${arg1}`;
            throw new Error(`Leg with ${indexes} could not be found.`);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        tryGetLeg(arg1, arg2) {
            return this._tryGetLeg(arg1, arg2);
        }
        /**
         * Attempts to get a leg from the flight plan.
         * @param arg1 The global leg index of the leg to get, or the index of the segment containing the leg to get.
         * @param arg2 The index of the leg to get in its segment.
         * @returns A flight plan leg, or `null` if one could not be found at the specified index.
         */
        _tryGetLeg(arg1, arg2) {
            var _a, _b;
            if (typeof arg1 === 'object' || arg2 !== undefined) {
                let segmentIndex;
                let segmentLegIndex;
                if (arg2 === undefined) {
                    ({ segmentIndex, segmentLegIndex } = arg1);
                }
                else {
                    segmentIndex = arg1;
                    segmentLegIndex = arg2;
                }
                return (_b = (_a = this.planSegments[segmentIndex]) === null || _a === void 0 ? void 0 : _a.legs[segmentLegIndex]) !== null && _b !== void 0 ? _b : null;
            }
            else {
                const legIndex = arg1;
                for (let i = 0; i < this.planSegments.length; i++) {
                    const segment = this.planSegments[i];
                    if (segment && segment.offset <= legIndex && legIndex < segment.offset + segment.legs.length) {
                        return segment.legs[legIndex - segment.offset];
                    }
                }
                return null;
            }
        }
        /**
         * Gets the global index of a flight plan leg in this flight plan.
         * @param leg A flight plan leg definition.
         * @returns the global index of the leg, or -1 if the leg is not in this flight plan.
         */
        getLegIndexFromLeg(leg) {
            let index = 0;
            for (const toCompare of this.legs()) {
                if (toCompare === leg) {
                    return index;
                }
                index++;
            }
            return -1;
        }
        /**
         * Gets the indexes describing the location of a flight plan leg in this flight plan.
         * @param leg The flight plan leg for which to get indexes.
         * @param out The object to which to write the result. If not defined, then a new index object will be created.
         * @returns The indexes describing the location of the specified flight plan leg in this flight plan. If the leg
         * is not in this flight plan, then all indexes will be equal to `-1`.
         */
        getLegIndexesFromLeg(leg, out) {
            out !== null && out !== void 0 ? out : (out = {
                segmentIndex: -1,
                segmentLegIndex: -1
            });
            for (let segmentIndex = 0; segmentIndex < this.planSegments.length; segmentIndex++) {
                const segment = this.planSegments[segmentIndex];
                if (segment !== undefined && segment.legs.length > 0) {
                    for (let segmentLegIndex = 0; segmentLegIndex < segment.legs.length; segmentLegIndex++) {
                        if (segment.legs[segmentLegIndex] === leg) {
                            out.segmentIndex = segmentIndex;
                            out.segmentLegIndex = segmentLegIndex;
                            return out;
                        }
                    }
                }
            }
            return out;
        }
        /**
         * Gets the indexes describing the location of a global leg index in this flight plan.
         * @param globalLegIndex The global leg index for which to get indexes.
         * @param out The object to which to write the result. If not defined, then a new index object will be created.
         * @returns The indexes describing the location of the specified global leg index this flight plan. If the global leg
         * index does not point to a valid leg in this flight plan, then all indexes will be equal to `-1`.
         */
        getLegIndexesFromGlobalLegIndex(globalLegIndex, out) {
            out !== null && out !== void 0 ? out : (out = {
                segmentIndex: -1,
                segmentLegIndex: -1
            });
            if (globalLegIndex >= 0 && globalLegIndex < this.length) {
                for (let i = 0; i < this.planSegments.length; i++) {
                    const segment = this.planSegments[i];
                    if (segment && segment.offset <= globalLegIndex && globalLegIndex < segment.offset + segment.legs.length) {
                        out.segmentIndex = segment.segmentIndex;
                        out.segmentLegIndex = globalLegIndex - segment.offset;
                        return out;
                    }
                }
            }
            return out;
        }
        /**
         * Gets the segment index for a given global leg index.
         * @param globalLegIndex The global leg index to get the segment index for.
         * @returns The segment index for the given global leg index, or -1 if not found.
         */
        getSegmentIndex(globalLegIndex) {
            return this.getLegIndexesFromGlobalLegIndex(globalLegIndex, this.legIndexesCache).segmentIndex;
        }
        /**
         * Gets the segment leg index (the index of the leg in its segment) for a given global leg index.
         * @param globalLegIndex The global leg index to get the segment leg index for.
         * @returns The segment leg index, or -1 if not found.
         */
        getSegmentLegIndex(globalLegIndex) {
            return this.getLegIndexesFromGlobalLegIndex(globalLegIndex, this.legIndexesCache).segmentLegIndex;
        }
        /**
         * Gets the flight plan segment to which a leg belongs.
         * @param leg A flight plan leg definition.
         * @returns The segment to which the leg belongs, or null if the leg is not in this flight plan.
         */
        getSegmentFromLeg(leg) {
            for (const segment of this.segments()) {
                if (segment.legs.includes(leg)) {
                    return segment;
                }
            }
            return null;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        getPrevLeg(arg1, arg2) {
            var _a, _b;
            let segmentIndex;
            let segmentLegIndex;
            if (arg2 === undefined) {
                ({ segmentIndex, segmentLegIndex } = arg1);
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
            }
            if (segmentIndex < 0) {
                return null;
            }
            segmentIndex = Math.min(segmentIndex, this.planSegments.length);
            segmentLegIndex = Math.min(segmentLegIndex, (_b = (_a = this.planSegments[segmentIndex]) === null || _a === void 0 ? void 0 : _a.legs.length) !== null && _b !== void 0 ? _b : 0);
            let segment = this.planSegments[segmentIndex];
            let leg = segment === null || segment === void 0 ? void 0 : segment.legs[segmentLegIndex - 1];
            while (!leg && --segmentIndex >= 0) {
                segment = this.planSegments[segmentIndex];
                if (segment) {
                    leg = segment.legs[segment.legs.length - 1];
                }
            }
            return leg !== null && leg !== void 0 ? leg : null;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        getNextLeg(arg1, arg2) {
            let segmentIndex;
            let segmentLegIndex;
            if (arg2 === undefined) {
                ({ segmentIndex, segmentLegIndex } = arg1);
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
            }
            if (segmentIndex >= this.planSegments.length) {
                return null;
            }
            segmentIndex = Math.max(segmentIndex, -1);
            segmentLegIndex = Math.max(segmentLegIndex, -1);
            let segment = this.planSegments[segmentIndex];
            let leg = segment === null || segment === void 0 ? void 0 : segment.legs[segmentLegIndex + 1];
            while (!leg && ++segmentIndex < this.planSegments.length) {
                segment = this.planSegments[segmentIndex];
                if (segment) {
                    leg = segment.legs[0];
                }
            }
            return leg !== null && leg !== void 0 ? leg : null;
        }
        /**
         * Finds a leg in this flight plan that satisfies a given condition.
         * @param predicate A function that evaluates whether each leg satisfies the condition to be returned by this method.
         * The function is called once for each evaluated leg and should return `true` if the leg satisfies the condition and
         * `false` otherwise.
         * @param reverse Whether to evaluate the legs in reverse order. Defaults to `false`.
         * @param startIndex The global leg index of the first leg to evaluate, inclusive. Defaults to `0` if `reverse` is
         * `false` or `this.length - 1` if `reverse` is `true`.
         * @param endIndex The global leg index of the last leg to evaluate, exclusive. Defaults to `this.length` if `reverse`
         * is `false` or `-1` if `reverse` is `true`.
         * @returns The first evaluated leg in this flight plan that satisfies the condition defined by the specified
         * predicate function, or `null` if no leg satisfies the condition.
         */
        findLeg(predicate, reverse = false, startIndex, endIndex) {
            return reverse
                ? this.findLegReverse(predicate, startIndex, endIndex)
                : this.findLegForward(predicate, startIndex, endIndex);
        }
        /**
         * Evaluates legs in this flight plan in forward order and returns the first evaluated leg that satisfies a given
         * condition.
         * @param predicate A function that evaluates whether each leg satisfies the condition to be returned by this method.
         * The function is called once for each evaluated leg and should return `true` if the leg satisfies the condition and
         * `false` otherwise.
         * @param startIndex The global leg index of the first leg to evaluate, inclusive. Defaults to `0`.
         * @param endIndex The global leg index of the last leg to evaluate, exclusive. Defaults to `this.length`.
         * @returns The first evaluated leg in this flight plan that satisfies the condition defined by the specified
         * predicate function, or `null` if no leg satisfies the condition.
         */
        findLegForward(predicate, startIndex = 0, endIndex = this.length) {
            endIndex = Math.min(this.length, endIndex);
            for (let segmentIndex = 0; segmentIndex < this.planSegments.length; segmentIndex++) {
                const segment = this.planSegments[segmentIndex];
                if (segment !== undefined && segment.legs.length > 0) {
                    const end = Math.min(segment.legs.length, endIndex - segment.offset);
                    if (end <= 0) {
                        return null;
                    }
                    for (let segmentLegIndex = Math.max(0, startIndex - segment.offset); segmentLegIndex < end; segmentLegIndex++) {
                        if (predicate(segment.legs[segmentLegIndex], segment, segment.segmentIndex, segmentLegIndex, this)) {
                            return segment.legs[segmentLegIndex];
                        }
                    }
                }
            }
            return null;
        }
        /**
         * Evaluates legs in this flight plan in reverse order and returns the first evaluated leg that satisfies a given
         * condition.
         * @param predicate A function that evaluates whether each leg satisfies the condition to be returned by this method.
         * The function is called once for each evaluated leg and should return `true` if the leg satisfies the condition and
         * `false` otherwise.
         * @param startIndex The global leg index of the first leg to evaluate, inclusive. Defaults to `this.length - 1`.
         * @param endIndex The global leg index of the last leg to evaluate, exclusive. Defaults to `-1`.
         * @returns The first evaluated leg in this flight plan that satisfies the condition defined by the specified
         * predicate function, or `null` if no leg satisfies the condition.
         */
        findLegReverse(predicate, startIndex = this.length - 1, endIndex = -1) {
            endIndex = Math.max(-1, endIndex);
            for (let segmentIndex = this.planSegments.length - 1; segmentIndex > -1; segmentIndex--) {
                const segment = this.planSegments[segmentIndex];
                if (segment !== undefined && segment.legs.length > 0) {
                    const end = Math.max(-1, endIndex - segment.offset);
                    if (end >= segment.legs.length) {
                        return null;
                    }
                    for (let segmentLegIndex = Math.min(segment.legs.length - 1, startIndex - segment.offset); segmentLegIndex > end; segmentLegIndex--) {
                        if (predicate(segment.legs[segmentLegIndex], segment, segment.segmentIndex, segmentLegIndex, this)) {
                            return segment.legs[segmentLegIndex];
                        }
                    }
                }
            }
            return null;
        }
        /**
         * Gets user data from the flight plan.
         * @param key The key of the user data.
         * @returns The user data, if found.
         */
        getUserData(key) {
            return this.userData[key];
        }
        /**
         * Gets the current modification batch stack.
         * @returns The current modification batch stack, or `undefined` if there are no open batches.
         */
        getBatchStack() {
            return this.batchEntryStack.length > 0 ? this.batchEntryStack.map(entry => entry.batch) : undefined;
        }
        /**
         * Opens a new modification batch. The new batch will be nested within the most recently opened batch that has not
         * yet been closed. While a batch is open, all modifications made to this flight plan will be assigned to the batch.
         * The use of batches is informational only and will not affect the timing or execution of flight plan modifications.
         * All modifications are carried out immediately when the corresponding method is called regardless of whether they
         * are assigned to a batch.
         * @param name The name to assign to the new batch.
         * @returns The UUID of the new batch.
         */
        openBatch(name) {
            const parent = this.batchEntryStack[this.batchEntryStack.length - 1];
            const uuid = UUID.GenerateUuid();
            const batch = { uuid, name };
            const entry = {
                batch,
                parent,
                isClosed: false,
                pendingCalculateCount: 0,
            };
            this.batchEntryStack.push(entry);
            this.events.onBatchOpened && this.events.onBatchOpened(batch);
            return uuid;
        }
        /**
         * Closes a modification batch.
         * @param uuid The UUID of the batch to close. If defined, then the batch with the specified UUID and all open
         * batches that are nested within it will be closed (with the most-nested batch closed first, followed by the next
         * most-nested batch, and so on). If not defined, then the most recently opened batch that has not yet been closed
         * (i.e. the most-nested open batch) will be closed.
         */
        closeBatch(uuid) {
            let stopIndex = this.batchEntryStack.length;
            if (uuid === undefined) {
                if (this.batchEntryStack.length > 0) {
                    stopIndex = this.batchEntryStack.length - 1;
                }
            }
            else {
                const index = this.batchEntryStack.findIndex(entry => entry.batch.uuid === uuid);
                if (index >= 0) {
                    stopIndex = index;
                }
            }
            this.closeBatchIndex(stopIndex);
        }
        /**
         * Closes all currently open modification batches.
         */
        closeAllBatches() {
            this.closeBatchIndex(0);
        }
        /**
         * Closes an indexed modification batch and all batches that are nested within it.
         * @param index The index of the batch to close.
         */
        closeBatchIndex(index) {
            index = Math.max(index, 0);
            // Check if a batch closure operation is currently in progress. If one is not, then start a new batch closure
            // operation. If one is in progress, then mark the index of the new batch to close if and only if it is less-
            // nested than the in-progress operation's current batch to close.
            if (this.batchToCloseIndex === undefined) {
                this.batchToCloseIndex = index;
                while (this.batchEntryStack.length > this.batchToCloseIndex) {
                    const entry = this.batchEntryStack.pop();
                    const currentBatchToCloseIndex = this.batchToCloseIndex;
                    const isClosingLast = this.batchEntryStack.length === currentBatchToCloseIndex + 1;
                    entry.isClosed = true;
                    this.events.onBatchClosed && this.events.onBatchClosed(entry.batch);
                    if (entry.pendingCalculateCount === 0) {
                        this.events.onBatchAsyncClosed && this.events.onBatchAsyncClosed(entry.batch);
                    }
                    // If we closed the last batch and no less-nested batches were marked for closure, then we will end the
                    // loop. This is to prevent us from closing any batches that were newly opened in the callbacks for closing
                    // the last batch.
                    if (isClosingLast && this.batchToCloseIndex === currentBatchToCloseIndex) {
                        break;
                    }
                }
                this.batchToCloseIndex = undefined;
            }
            else {
                this.batchToCloseIndex = Math.min(this.batchToCloseIndex, index);
            }
        }
        /**
         * Adds a segment to the flight plan at a specified index.
         * @param segmentIndex The index of the flight plan segment.
         * @param segmentType The type of segment this will be.
         * @param airway The airway this segment is made up of, if any.
         * @param notify Whether or not to send notifications after the operation.
         * @returns The new flight plan segment.
         */
        addSegment(segmentIndex, segmentType = exports.FlightPlanSegmentType.Enroute, airway, notify = true) {
            const segment = new FlightPlanSegment(segmentIndex, -1, [], segmentType, airway);
            this.planSegments[segmentIndex] = segment;
            this.reflowSegmentOffsets();
            notify && this.events.onSegmentChanged && this.events.onSegmentChanged(segmentIndex, exports.SegmentEventType.Added, segment, this.getBatchStack());
            return segment;
        }
        /**
         * Inserts a segment into the flight plan at the specified index and
         * reflows the subsequent segments.
         * @param segmentIndex The index to insert the flight plan segment.
         * @param segmentType The type of segment this will be.
         * @param airway The airway this segment is made up of, if any
         * @param notify Whether or not to send notifications after the operation.
         * @returns The new flight plan segment.
         */
        insertSegment(segmentIndex, segmentType = exports.FlightPlanSegmentType.Enroute, airway, notify = true) {
            const segment = this.planSegments[segmentIndex];
            if (segment !== undefined) {
                const newSegment = new FlightPlanSegment(segmentIndex, -1, [], segmentType, airway);
                this.planSegments.splice(segmentIndex, 0, newSegment);
                this.reflowSegments();
                this.reflowSegmentOffsets();
                notify && this.events.onSegmentChanged && this.events.onSegmentChanged(segmentIndex, exports.SegmentEventType.Inserted, newSegment, this.getBatchStack());
                return newSegment;
            }
            else {
                return this.addSegment(segmentIndex, segmentType, airway, notify);
            }
        }
        /**
         * Reflows the flight plan segments after an insert.
         */
        reflowSegments() {
            for (let i = 0; i < this.planSegments.length; i++) {
                const segment = this.planSegments[i];
                if (segment !== undefined && segment.segmentIndex !== i) {
                    segment.segmentIndex = i;
                }
            }
        }
        /**
         * Deletes a segment from the flight plan, leaving an empty segment at the specified index.
         * @param segmentIndex The index of the segment to remove.
         * @param notify Whether or not to send notifications after the operation.
         */
        deleteSegment(segmentIndex, notify = true) {
            const segment = this.planSegments[segmentIndex];
            if (segmentIndex === this.planSegments.length - 1) {
                this.planSegments.splice(segmentIndex, 1);
            }
            else {
                delete this.planSegments[segmentIndex];
            }
            if (this.directToData.segmentIndex === segmentIndex) ;
            this.reflowSegmentOffsets();
            notify && this.events.onSegmentChanged && this.events.onSegmentChanged(segmentIndex, exports.SegmentEventType.Removed, segment, this.getBatchStack());
        }
        /**
         * Removes a segment from the flight plan and reflows the segments following
         * the removed segment, not leaving an empty segment at the specified index.
         * @param segmentIndex The index of the segment to remove.
         * @param notify Whether or not to send notifications after the operation.
         */
        removeSegment(segmentIndex, notify = true) {
            const segment = this.planSegments[segmentIndex];
            this.planSegments.splice(segmentIndex, 1);
            this.reflowSegments();
            this.reflowSegmentOffsets();
            notify && this.events.onSegmentChanged && this.events.onSegmentChanged(segmentIndex, exports.SegmentEventType.Removed, segment, this.getBatchStack());
        }
        /**
         * Adds a leg to the flight plan.
         * @param segmentIndex The segment to add the leg to.
         * @param leg The leg to add to the plan.
         * @param segmentLegIndex The index of the leg in the segment to insert. Will add to the end of the segment if omitted.
         * @param flags Leg definition flags to apply to the new leg. Defaults to `None` (0).
         * @param notify Whether or not to send notifications after the operation.
         * @returns the leg that was added.
         */
        addLeg(segmentIndex, leg, segmentLegIndex, flags = 0, notify = true) {
            const segment = this.getSegment(segmentIndex);
            const legDefinition = {
                name: this.onLegNameRequested(leg),
                leg,
                flags,
                verticalData: {
                    phase: exports.VerticalFlightPhase.Descent,
                    altDesc: exports.AltitudeRestrictionType.Unused,
                    altitude1: 0,
                    altitude2: 0,
                    displayAltitude1AsFlightLevel: false,
                    displayAltitude2AsFlightLevel: false,
                    isAltitude1TempCompensated: false,
                    isAltitude2TempCompensated: false,
                    speedDesc: exports.SpeedRestrictionType.Unused,
                    speed: 0,
                    speedUnit: exports.SpeedUnit.IAS,
                },
                userData: {}
            };
            if (segmentLegIndex === undefined) {
                segment.legs.push(legDefinition);
                segmentLegIndex = segment.legs.length - 1;
            }
            else {
                segment.legs.splice(segmentLegIndex, 0, legDefinition);
            }
            this.reflowSegmentOffsets();
            notify && this.events.onLegChanged && this.events.onLegChanged(segmentIndex, segmentLegIndex, exports.LegEventType.Added, legDefinition, this.getBatchStack());
            return legDefinition;
        }
        /**
         * Removes a leg from the flight plan.
         * @param segmentIndex The segment to add the leg to.
         * @param segmentLegIndex The index of the leg in the segment to remove. Will remove from the end of the segment if ommitted.
         * @param notify Whether or not to send notifications after the operation.
         * @returns the leg that was removed, or null if a leg was not removed.
         */
        removeLeg(segmentIndex, segmentLegIndex, notify = true) {
            const segment = this.getSegment(segmentIndex);
            let legDefinition;
            if (segmentLegIndex === undefined) {
                legDefinition = segment.legs.pop();
                segmentLegIndex = segment.legs.length;
            }
            else {
                const deleted = segment.legs.splice(segmentLegIndex, 1);
                legDefinition = deleted[0];
            }
            if (this.directToData.segmentIndex === segmentIndex && this.directToData.segmentLegIndex === segmentLegIndex) ;
            this.reflowSegmentOffsets();
            notify && legDefinition && this.events.onLegChanged && this.events.onLegChanged(segmentIndex, segmentLegIndex, exports.LegEventType.Removed, legDefinition, this.getBatchStack());
            return legDefinition !== null && legDefinition !== void 0 ? legDefinition : null;
        }
        /**
         * Reflows all flight plan segment offsets after a plan change.
         */
        reflowSegmentOffsets() {
            let nextOffset = undefined;
            for (let i = 0; i < this.planSegments.length; i++) {
                const segment = this.planSegments[i];
                if (segment) {
                    if (nextOffset === undefined) {
                        segment.offset = 0;
                    }
                    else {
                        segment.offset = nextOffset;
                    }
                    nextOffset = segment.legs.length + segment.offset;
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setOriginAirport(facilityIcao, notify = true) {
            if (typeof facilityIcao === 'object') {
                this._originAirportIcao = facilityIcao;
                this._originAirport = ICAO.valueToStringV1(facilityIcao);
            }
            else {
                this._originAirportIcao = ICAO.stringV1ToValue(facilityIcao);
                this._originAirport = facilityIcao;
            }
            notify && this.events.onOriginDestChanged
                && this.events.onOriginDestChanged(exports.OriginDestChangeType.OriginAdded, this._originAirportIcao, this.getBatchStack());
        }
        /**
         * Removes the origin airport from the flight plan.
         * @param notify Whether to notify subscribers of the change. Defaults to `true`.
         */
        removeOriginAirport(notify = true) {
            const facilityIcao = this._originAirportIcao;
            this._originAirportIcao = undefined;
            this._originAirport = undefined;
            this.procedureDetails.departureIndex = -1;
            this.procedureDetails.departureRunwayIndex = -1;
            this.procedureDetails.departureTransitionIndex = -1;
            this.procedureDetails.originRunway = undefined;
            notify && this.events.onOriginDestChanged
                && this.events.onOriginDestChanged(exports.OriginDestChangeType.OriginRemoved, facilityIcao, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setDestinationAirport(facilityIcao, notify = true) {
            if (typeof facilityIcao === 'object') {
                this._destinationAirportIcao = facilityIcao;
                this._destinationAirport = ICAO.valueToStringV1(facilityIcao);
            }
            else {
                this._destinationAirportIcao = ICAO.stringV1ToValue(facilityIcao);
                this._destinationAirport = facilityIcao;
            }
            notify && this.events.onOriginDestChanged &&
                this.events.onOriginDestChanged(exports.OriginDestChangeType.DestinationAdded, this._destinationAirportIcao, this.getBatchStack());
        }
        /**
         * Removes the destination airport from the flight plan.
         * @param notify Whether to notify subscribers of the change. Defaults to `true`.
         */
        removeDestinationAirport(notify = true) {
            const facilityIcao = this._destinationAirportIcao;
            this._destinationAirportIcao = undefined;
            this._destinationAirport = undefined;
            this.procedureDetails.approachIndex = -1;
            this.procedureDetails.approachTransitionIndex = -1;
            this.procedureDetails.arrivalIndex = -1;
            this.procedureDetails.arrivalRunwayTransitionIndex = -1;
            this.procedureDetails.arrivalRunway = undefined;
            this.procedureDetails.arrivalTransitionIndex = -1;
            this.procedureDetails.destinationRunway = undefined;
            notify && this.events.onOriginDestChanged
                && this.events.onOriginDestChanged(exports.OriginDestChangeType.DestinationRemoved, facilityIcao, this.getBatchStack());
        }
        /**
         * Sets the active lateral leg index in the flight plan.
         * @param globalLegIndex The global leg index to set.
         * @param notify Whether or not to send notifications after the operation.
         */
        setLateralLeg(globalLegIndex, notify = true) {
            let previousLegIndex = -1;
            let previousSegmentIndex = -1;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            if (this.length > 0) {
                previousSegmentIndex = this.getSegmentIndex(this._activeLateralLeg);
                if (previousSegmentIndex > -1) {
                    previousLegIndex = this._activeLateralLeg - this.getSegment(previousSegmentIndex).offset;
                }
                this._activeLateralLeg = Utils.Clamp(globalLegIndex, 0, this.length - 1);
                segmentIndex = this.getSegmentIndex(this._activeLateralLeg);
                if (segmentIndex > -1) {
                    segmentLegIndex = this._activeLateralLeg - this.getSegment(segmentIndex).offset;
                }
            }
            else {
                this._activeLateralLeg = 0;
            }
            notify
                && this.events.onActiveLegChanged
                && this.events.onActiveLegChanged(this._activeLateralLeg, segmentIndex, segmentLegIndex, previousSegmentIndex, previousLegIndex, exports.ActiveLegType.Lateral, this.getBatchStack());
        }
        /**
         * Sets the active lateral leg index in the flight plan.
         * @param globalLegIndex The global leg index to set.
         * @param notify Whether or not to send notifications after the operation.
         */
        setVerticalLeg(globalLegIndex, notify = true) {
            let previousLegIndex = -1;
            let previousSegmentIndex = -1;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            if (this.length > 0) {
                previousSegmentIndex = this.getSegmentIndex(this._activeVerticalLeg);
                if (previousSegmentIndex > -1) {
                    previousLegIndex = this._activeVerticalLeg - this.getSegment(previousSegmentIndex).offset;
                }
                this._activeVerticalLeg = Utils.Clamp(globalLegIndex, 0, this.length - 1);
                segmentIndex = this.getSegmentIndex(this._activeVerticalLeg);
                if (segmentIndex > -1) {
                    segmentLegIndex = this._activeVerticalLeg - this.getSegment(segmentIndex).offset;
                }
            }
            else {
                this._activeVerticalLeg = 0;
            }
            notify
                && this.events.onActiveLegChanged
                && this.events.onActiveLegChanged(this._activeVerticalLeg, segmentIndex, segmentLegIndex, previousSegmentIndex, previousLegIndex, exports.ActiveLegType.Vertical, this.getBatchStack());
        }
        /**
         * Sets the active calculating leg index in the flight plan.
         * @param globalLegIndex The global leg index to set.
         * @param notify Whether or not to send notifications after the operation.
         */
        setCalculatingLeg(globalLegIndex, notify = true) {
            let previousLegIndex = -1;
            let previousSegmentIndex = -1;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            if (this.length > 0) {
                previousSegmentIndex = this.getSegmentIndex(this._activeCalculatingLeg);
                if (previousSegmentIndex > -1) {
                    previousLegIndex = this._activeCalculatingLeg - this.getSegment(previousSegmentIndex).offset;
                }
                this._activeCalculatingLeg = Utils.Clamp(globalLegIndex, 0, this.length - 1);
                segmentIndex = this.getSegmentIndex(this._activeCalculatingLeg);
                if (segmentIndex > -1) {
                    segmentLegIndex = this._activeCalculatingLeg - this.getSegment(segmentIndex).offset;
                }
            }
            else {
                this._activeCalculatingLeg = 0;
            }
            //PM Modif issue when the flight plan contains only 1 waypoint
            // We then continuously notfy an activate leg to 0
            // The fix is to not notify in this situation
            if(notify && this.length == 1 && this.previousActiveCalculatingLeg != undefined && this.previousActiveCalculatingLeg == 0 && this._activeCalculatingLeg == 0 ) {
                notify = false;
            }
            else
                this.previousActiveCalculatingLeg = this._activeCalculatingLeg;
            //PM Modif End
            notify
                && this.events.onActiveLegChanged
                && this.events.onActiveLegChanged(this._activeCalculatingLeg, segmentIndex, segmentLegIndex, previousSegmentIndex, previousLegIndex, exports.ActiveLegType.Calculating, this.getBatchStack());
        }
        /**
         * Sets this flight plan's procedure details.
         * @param details An object containing the procedure details properties to set. Properties omitted from the object
         * (i.e. that are not enumerable) will not be set. Properties that are present in the object (including those whose
         * values are equal to `undefined`) will set the corresponding properties in the flight plan procedure details with
         * the same values.
         * @param notify Whether to notify subscribers of the change. Defaults to `true`.
         */
        setProcedureDetails(details, notify = true) {
            // Temporary fix to ensure the various ICAO struct properties are always in sync with the ICAO string properties.
            // TODO: Remove this when the deprecated ICAO string properties are removed.
            const modifiedDetails = Object.assign({}, details);
            this.reconcileProcedureDetailsIcaoPropPair(modifiedDetails, 'departureFacilityIcaoStruct', 'departureFacilityIcao');
            this.reconcileProcedureDetailsIcaoPropPair(modifiedDetails, 'arrivalFacilityIcaoStruct', 'arrivalFacilityIcao');
            this.reconcileProcedureDetailsIcaoPropPair(modifiedDetails, 'approachFacilityIcaoStruct', 'approachFacilityIcao');
            // We iterate of the keys of `details` because we need to be able to set fields to undefined
            // and we only want to overwrite fields that were in the `details` object
            for (const key of Object.keys(modifiedDetails)) {
                this.procedureDetails[key] = modifiedDetails[key];
            }
            notify && this.events.onProcedureDetailsChanged && this.events.onProcedureDetailsChanged(this.procedureDetails, this.getBatchStack());
        }
        /**
         * Reconciles a pair of associated ICAO properties in a `ProcedureDetails` object. If one of the pair is defined but
         * the other is not, then the undefined property will be added to the object with a value derived from its defined
         * partner. If both of the pair are defined but have incompatible values, then the string (V1) property will be
         * redefined to be compatible with the struct property. Otherwise, the object will remain unchanged.
         * @param details The object containing the properties to reconcile.
         * @param icaoStructProp The name of the ICAO struct property to reconcile.
         * @param icaoStringProp The name of the ICAO string (V1) property to reconcile.
         */
        reconcileProcedureDetailsIcaoPropPair(details, icaoStructProp, icaoStringProp) {
            const isStructPropDefined = icaoStructProp in details;
            const isStringPropDefined = icaoStringProp in details;
            if (isStructPropDefined && !isStringPropDefined) {
                // Struct prop is defined but string prop is not -> need to set string prop from struct prop
                details[icaoStringProp] = details[icaoStructProp]
                    ? ICAO.tryValueToStringV1(details[icaoStructProp])
                    : undefined;
            }
            else if (!isStructPropDefined && isStringPropDefined) {
                // String prop is defined but struct prop is not -> need to set struct prop from string prop
                details[icaoStructProp] = details[icaoStringProp] !== undefined
                    ? ICAO.stringV1ToValue(details[icaoStringProp])
                    : undefined;
            }
            else if (isStructPropDefined && isStringPropDefined) {
                const structProp = details[icaoStructProp];
                const stringProp = details[icaoStringProp];
                if ((structProp !== undefined && stringProp === undefined)
                    || (structProp === undefined && stringProp !== undefined)
                    || (structProp !== undefined && stringProp !== undefined && ICAO.valueToStringV1(structProp) !== stringProp)) {
                    // Both struct and string props are defined but their values are incompatible -> need to set string prop from struct prop
                    details[icaoStringProp] = structProp
                        ? ICAO.tryValueToStringV1(structProp)
                        : undefined;
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setDirectToData(arg1, arg2, arg3) {
            if (typeof arg1 !== 'number') {
                // arg1 is a LegDefinition or null.
                arg1 = arg1 ? this.getLegIndexFromLeg(arg1) : -1;
            }
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            let notify = true;
            if (typeof arg2 !== 'number') {
                const globalLegIndex = arg1;
                if (globalLegIndex >= 0) {
                    segmentIndex = this.getSegmentIndex(globalLegIndex);
                    if (segmentIndex >= 0) {
                        segmentLegIndex = globalLegIndex - this.getSegment(segmentIndex).offset;
                    }
                }
                notify = arg2 !== null && arg2 !== void 0 ? arg2 : true;
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
                notify = arg3 !== null && arg3 !== void 0 ? arg3 : true;
            }
            this.directToData.segmentIndex = segmentIndex;
            this.directToData.segmentLegIndex = segmentLegIndex;
            notify && this.events.onDirectDataChanged && this.events.onDirectDataChanged(this.directToData, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setLegVerticalData(arg1, arg2, arg3, arg4) {
            let notify = true;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            let verticalData;
            if (typeof arg2 !== 'number') {
                segmentIndex = this.getSegmentIndex(arg1);
                const segment = this.getSegment(segmentIndex);
                segmentLegIndex = arg1 - segment.offset;
                verticalData = arg2;
                notify = arg3 !== undefined ? arg3 : notify;
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
                verticalData = arg3;
                notify = arg4 !== undefined ? arg4 : notify;
            }
            const leg = this.tryGetLeg(segmentIndex, segmentLegIndex);
            if (leg) {
                Object.assign(leg.verticalData, verticalData);
                notify && this.events.onLegChanged && this.events.onLegChanged(segmentIndex, segmentLegIndex, exports.LegEventType.Changed, leg, this.getBatchStack());
            }
            else {
                console.warn(`Failed to set Leg Vertical Data for Segment ${segmentIndex} Leg ${segmentLegIndex}.`);
            }
        }
        /**
         * Sets the airway for a segment.
         * @param segmentIndex A segment index.
         * @param airway The airway name.
         * @param notify is whether to send an event for this change
         */
        setAirway(segmentIndex, airway, notify = true) {
            const segment = this.getSegment(segmentIndex);
            if (!airway) {
                segment.airway = undefined;
            }
            else {
                segment.airway = airway;
            }
            this.events.onSegmentChanged && notify && this.events.onSegmentChanged(segmentIndex, exports.SegmentEventType.Changed, segment, this.getBatchStack());
        }
        /**
         * Sets a global key-value user data pair for this flight plan. Setting a key's user data to `undefined` will delete
         * the key instead.
         * @param key The key of the user data.
         * @param data The data to set.
         * @param notify Whether or not to notify subscribers. Defaults to true.
         */
        setUserData(key, data, notify = true) {
            if (data === undefined) {
                this.deleteUserData(key, notify);
                return;
            }
            this.userData[key] = data;
            this.events.onUserDataSet && notify && this.events.onUserDataSet(key, data, this.getBatchStack());
        }
        /**
         * Deletes a global key-value user data pair for this flight plan.
         * @param key The key to delete.
         * @param notify Whether or not to notify subscribers.
         */
        deleteUserData(key, notify = true) {
            if (this.userData[key] !== undefined) {
                delete this.userData[key];
            }
            this.events.onUserDataDelete && notify && this.events.onUserDataDelete(key, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setLegUserData(arg1, arg2, arg3, arg4, arg5) {
            let notify;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            let key;
            let data;
            if (typeof arg2 !== 'number') {
                segmentIndex = this.getSegmentIndex(arg1);
                const segment = this.getSegment(segmentIndex);
                segmentLegIndex = arg1 - segment.offset;
                key = arg2;
                data = arg3;
                notify = arg4 === undefined ? true : arg4;
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
                key = arg3;
                data = arg4;
                notify = arg5 === undefined ? true : arg5;
            }
            if (data === undefined) {
                this.deleteLegUserData(segmentIndex, segmentLegIndex, key, notify);
                return;
            }
            const leg = this.tryGetLeg(segmentIndex, segmentLegIndex);
            if (leg) {
                leg.userData[key] = data;
                notify && this.events.onLegUserDataSet && this.events.onLegUserDataSet(segmentIndex, segmentLegIndex, leg, key, data, this.getBatchStack());
            }
            else {
                console.warn(`FlightPlan: failed to set leg user data; leg does not exist at segment index ${segmentIndex}, segment leg index ${segmentLegIndex}.`);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        deleteLegUserData(arg1, arg2, arg3, arg4) {
            let notify;
            let segmentIndex = -1;
            let segmentLegIndex = -1;
            let key;
            if (typeof arg2 !== 'number') {
                segmentIndex = this.getSegmentIndex(arg1);
                const segment = this.getSegment(segmentIndex);
                segmentLegIndex = arg1 - segment.offset;
                key = arg2;
                notify = arg3 === undefined ? true : arg3;
            }
            else {
                segmentIndex = arg1;
                segmentLegIndex = arg2;
                key = arg3;
                notify = arg4 === undefined ? true : arg4;
            }
            const leg = this.tryGetLeg(segmentIndex, segmentLegIndex);
            if (leg) {
                delete leg.userData[key];
                notify && this.events.onLegUserDataDelete && this.events.onLegUserDataDelete(segmentIndex, segmentLegIndex, leg, key, this.getBatchStack());
            }
            else {
                console.warn(`FlightPlan: failed to delete leg user data; leg does not exist at segment index ${segmentIndex}, segment leg index ${segmentLegIndex}.`);
            }
        }
        /**
         * Sets the origin runway in procedure details.
         * @param runway The oneway runway to set as the origin, or undefined
         * @param notify Whether or not to notify subscribers.
         */
        setOriginRunway(runway = undefined, notify = true) {
            this.procedureDetails.originRunway = runway;
            const details = Object.assign(FlightPlan.createProcedureDetails(), this.procedureDetails);
            this.events.onProcedureDetailsChanged && notify && this.events.onProcedureDetailsChanged(details, this.getBatchStack());
        }
        /**
         * Sets the destination runway in procedure details.
         * @param runway The oneway runway to set as the destination, or undefined
         * @param notify Whether or not to notify subscribers.
         */
        setDestinationRunway(runway = undefined, notify = true) {
            this.procedureDetails.destinationRunway = runway;
            const details = Object.assign(FlightPlan.createProcedureDetails(), this.procedureDetails);
            this.events.onProcedureDetailsChanged && notify && this.events.onProcedureDetailsChanged(details, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setDeparture(facilityIcao, departureIndex = -1, departureTransitionIndex = -1, departureRunwayIndex = -1, notify = true) {
            this.procedureDetails.departureIndex = departureIndex;
            if (facilityIcao === undefined) {
                this.procedureDetails.departureFacilityIcaoStruct = undefined;
                this.procedureDetails.departureFacilityIcao = undefined;
            }
            else if (typeof facilityIcao === 'object') {
                this.procedureDetails.departureFacilityIcaoStruct = facilityIcao;
                this.procedureDetails.departureFacilityIcao = ICAO.valueToStringV1(facilityIcao);
            }
            else {
                this.procedureDetails.departureFacilityIcaoStruct = ICAO.stringV1ToValue(facilityIcao);
                this.procedureDetails.departureFacilityIcao = facilityIcao;
            }
            this.procedureDetails.departureTransitionIndex = departureTransitionIndex;
            this.procedureDetails.departureRunwayIndex = departureRunwayIndex;
            const details = Object.assign(FlightPlan.createProcedureDetails(), this.procedureDetails);
            this.events.onProcedureDetailsChanged && notify && this.events.onProcedureDetailsChanged(details, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setArrival(facilityIcao, arrivalIndex = -1, arrivalTransitionIndex = -1, arrivalRunwayTransitionIndex = -1, arrivalRunway, notify = true) {
            this.procedureDetails.arrivalIndex = arrivalIndex;
            if (facilityIcao === undefined) {
                this.procedureDetails.arrivalFacilityIcaoStruct = undefined;
                this.procedureDetails.arrivalFacilityIcao = undefined;
            }
            else if (typeof facilityIcao === 'object') {
                this.procedureDetails.arrivalFacilityIcaoStruct = facilityIcao;
                this.procedureDetails.arrivalFacilityIcao = ICAO.valueToStringV1(facilityIcao);
            }
            else {
                this.procedureDetails.arrivalFacilityIcaoStruct = ICAO.stringV1ToValue(facilityIcao);
                this.procedureDetails.arrivalFacilityIcao = facilityIcao;
            }
            this.procedureDetails.arrivalTransitionIndex = arrivalTransitionIndex;
            this.procedureDetails.arrivalRunwayTransitionIndex = arrivalRunwayTransitionIndex;
            this.procedureDetails.arrivalRunway = arrivalRunway;
            const details = Object.assign(FlightPlan.createProcedureDetails(), this.procedureDetails);
            this.events.onProcedureDetailsChanged && notify && this.events.onProcedureDetailsChanged(details, this.getBatchStack());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        setApproach(facilityIcao, approachIndex = -1, approachTransitionIndex = -1, notify = true) {
            this.procedureDetails.approachIndex = approachIndex;
            if (facilityIcao === undefined) {
                this.procedureDetails.approachFacilityIcaoStruct = undefined;
                this.procedureDetails.approachFacilityIcao = undefined;
            }
            else if (typeof facilityIcao === 'object') {
                this.procedureDetails.approachFacilityIcaoStruct = facilityIcao;
                this.procedureDetails.approachFacilityIcao = ICAO.valueToStringV1(facilityIcao);
            }
            else {
                this.procedureDetails.approachFacilityIcaoStruct = ICAO.stringV1ToValue(facilityIcao);
                this.procedureDetails.approachFacilityIcao = facilityIcao;
            }
            this.procedureDetails.approachIndex = approachIndex;
            this.procedureDetails.approachTransitionIndex = approachTransitionIndex;
            const details = Object.assign(FlightPlan.createProcedureDetails(), this.procedureDetails);
            this.events.onProcedureDetailsChanged && notify && this.events.onProcedureDetailsChanged(details, this.getBatchStack());
        }
        /**
         * Calculates the flight path for the plan.
         * @param globalLegIndex The global leg index to start calculating from.
         * @param notify Whether or not to send notifications after the operation.
         */
        async calculate(globalLegIndex, notify = true) {
            const batchStack = this.getBatchStack();
            const batchEntry = this.batchEntryStack[this.batchEntryStack.length - 1];
            let currentBatchEntry = batchEntry;
            while (currentBatchEntry) {
                currentBatchEntry.pendingCalculateCount++;
                currentBatchEntry = currentBatchEntry.parent;
            }
            notify && this.events.onCalculatePended && this.events.onCalculatePended(globalLegIndex, batchStack);
            const legs = [...this.legs()];
            try {
                await this.calculator.calculateFlightPath(legs, this.activeLateralLeg, globalLegIndex === undefined ? this.activeCalculatingLeg : globalLegIndex);
            }
            catch (e) {
                console.error(e);
                if (e instanceof Error) {
                    console.error(e.stack);
                }
            }
            notify && this.events.onCalculated && this.events.onCalculated(globalLegIndex, batchStack);
            currentBatchEntry = batchEntry;
            while (currentBatchEntry) {
                currentBatchEntry.pendingCalculateCount--;
                if (currentBatchEntry.isClosed && currentBatchEntry.pendingCalculateCount === 0) {
                    this.events.onBatchAsyncClosed && this.events.onBatchAsyncClosed(currentBatchEntry.batch);
                }
                currentBatchEntry = currentBatchEntry.parent;
            }
        }
        /**
         * Copies the flight plan.
         * @param planIndex The flight plan index to assign to this plan, or the same plan
         * index if not provided.
         * @param copyCalcs Whether to copy leg calculations (defaults to false).
         * @returns The copied flight plan.
         */
        copy(planIndex, copyCalcs = false) {
            if (planIndex === undefined) {
                planIndex = this.planIndex;
            }
            const newPlan = new FlightPlan(planIndex, this.calculator, this.onLegNameRequested);
            newPlan.copyFrom(this, copyCalcs);
            return newPlan;
        }
        /**
         * Copies a source flight plan into this one, overriding everything in this plan with everything from the source plan.
         * @param sourcePlan The plan to copy from.
         * @param copyCalcs Whether to copy leg calculations (defaults to false).
         */
        copyFrom(sourcePlan, copyCalcs = false) {
            if (sourcePlan._originAirportIcao !== undefined) {
                this.setOriginAirport(sourcePlan._originAirportIcao, false);
            }
            else {
                this.removeOriginAirport(false);
            }
            if (sourcePlan._destinationAirportIcao !== undefined) {
                this.setDestinationAirport(sourcePlan._destinationAirportIcao, false);
            }
            else {
                this.removeDestinationAirport(false);
            }
            // We do object assign against new proc details in case the incoming details are missing fields because of coming from json
            // and because we want to overwrite the entire object, instead of just some fields.
            this.setProcedureDetails(Object.assign(FlightPlan.createProcedureDetails(), sourcePlan.procedureDetails), false);
            const targetPlanSegmentsCount = this.planSegments.length;
            for (let i = 0; i < targetPlanSegmentsCount; i++) {
                this.removeSegment(0, false);
            }
            for (let i = 0; i < sourcePlan.planSegments.length; i++) {
                const segment = sourcePlan.planSegments[i];
                if (segment !== undefined) {
                    this.addSegment(segment.segmentIndex, segment.segmentType, segment.airway, false);
                    for (const leg of segment.legs) {
                        const newLeg = this.addLeg(segment.segmentIndex, leg.leg, undefined, leg.flags, false);
                        const legIndex = this.getLegIndexFromLeg(newLeg);
                        this.setLegVerticalData(legIndex, leg.verticalData, false);
                        for (const key in leg.userData) {
                            this.setLegUserData(legIndex, key, leg.userData[key], false);
                        }
                        copyCalcs && FlightPlan.copyLegCalculations(leg, newLeg);
                    }
                }
            }
            this.setDirectToData(sourcePlan.directToData.segmentIndex, sourcePlan.directToData.segmentLegIndex, false);
            // Have to copy from the private fields, because the public ones are getters, which would be lost when stringified
            this.setLateralLeg(sourcePlan._activeLateralLeg, false);
            this.setVerticalLeg(sourcePlan._activeVerticalLeg, false);
            this.setCalculatingLeg(sourcePlan._activeCalculatingLeg, false);
            for (const key in this.userData) {
                this.deleteUserData(key, false);
            }
            for (const key in sourcePlan.userData) {
                this.setUserData(key, sourcePlan.userData[key], false);
            }
        }
        /**
         * Copies leg calcs from an existing leg to a new leg.
         * @param existingLeg The leg that we want to copy the calcs from.
         * @param newLeg The leg that we want to copy the calcs to.
         * @returns the newLeg with the copied calcs.
         */
        static copyLegCalculations(existingLeg, newLeg) {
            if (existingLeg.calculated !== undefined) {
                newLeg.calculated = {
                    courseMagVar: existingLeg.calculated.courseMagVar,
                    initialDtk: existingLeg.calculated.initialDtk,
                    distance: existingLeg.calculated.distance,
                    cumulativeDistance: existingLeg.calculated.cumulativeDistance,
                    distanceWithTransitions: existingLeg.calculated.distanceWithTransitions,
                    cumulativeDistanceWithTransitions: existingLeg.calculated.cumulativeDistanceWithTransitions,
                    startLat: existingLeg.calculated.startLat,
                    startLon: existingLeg.calculated.startLon,
                    endLat: existingLeg.calculated.endLat,
                    endLon: existingLeg.calculated.endLon,
                    flightPath: existingLeg.calculated.flightPath.map(vector => Object.assign({}, vector)),
                    ingress: existingLeg.calculated.ingress.map(vector => Object.assign({}, vector)),
                    ingressJoinIndex: existingLeg.calculated.ingressJoinIndex,
                    ingressToEgress: existingLeg.calculated.ingressToEgress.map(vector => Object.assign({}, vector)),
                    egressJoinIndex: existingLeg.calculated.egressJoinIndex,
                    egress: existingLeg.calculated.egress.map(vector => Object.assign({}, vector)),
                    endsInDiscontinuity: existingLeg.calculated.endsInDiscontinuity,
                    endsInFallback: existingLeg.calculated.endsInFallback
                };
            }
            return newLeg;
        }
        /**
         * Creates a new flight plan leg.
         * @param partial The properties to initialize on the new leg. Any undefined properties will be initialized to
         * default values. For ICAO property pairs - struct and string (V1) - only one of each pair needs to be defined; the
         * other property in the pair will be set from the property that is defined. If paired ICAO properties are not
         * consistent with each other, then the struct property will take precedence.
         * @returns A new flight plan leg.
         */
        static createLeg(partial) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
            partial = Object.assign({}, partial);
            FlightPlan.reconcileFlightPlanLegIcaoPropPair(partial, 'fixIcaoStruct', 'fixIcao');
            FlightPlan.reconcileFlightPlanLegIcaoPropPair(partial, 'originIcaoStruct', 'originIcao');
            FlightPlan.reconcileFlightPlanLegIcaoPropPair(partial, 'arcCenterFixIcaoStruct', 'arcCenterFixIcao');
            return {
                type: (_a = partial.type) !== null && _a !== void 0 ? _a : exports.LegType.Unknown,
                fixIcao: (_b = partial.fixIcao) !== null && _b !== void 0 ? _b : ICAO.EMPTY_V1,
                fixIcaoStruct: (_c = partial.fixIcaoStruct) !== null && _c !== void 0 ? _c : ICAO.emptyValue(),
                arcCenterFixIcao: (_d = partial.arcCenterFixIcao) !== null && _d !== void 0 ? _d : ICAO.EMPTY_V1,
                arcCenterFixIcaoStruct: (_e = partial.arcCenterFixIcaoStruct) !== null && _e !== void 0 ? _e : ICAO.emptyValue(),
                originIcao: (_f = partial.originIcao) !== null && _f !== void 0 ? _f : ICAO.EMPTY_V1,
                originIcaoStruct: (_g = partial.originIcaoStruct) !== null && _g !== void 0 ? _g : ICAO.emptyValue(),
                flyOver: (_h = partial.flyOver) !== null && _h !== void 0 ? _h : false,
                turnDirection: (_j = partial.turnDirection) !== null && _j !== void 0 ? _j : exports.LegTurnDirection.None,
                trueDegrees: (_k = partial.trueDegrees) !== null && _k !== void 0 ? _k : false,
                theta: (_l = partial.theta) !== null && _l !== void 0 ? _l : 0,
                rho: (_m = partial.rho) !== null && _m !== void 0 ? _m : 0,
                distance: (_o = partial.distance) !== null && _o !== void 0 ? _o : 0,
                distanceMinutes: (_p = partial.distanceMinutes) !== null && _p !== void 0 ? _p : false,
                speedRestriction: (_q = partial.speedRestriction) !== null && _q !== void 0 ? _q : 0,
                speedRestrictionDesc: (_r = partial.speedRestrictionDesc) !== null && _r !== void 0 ? _r : exports.SpeedRestrictionType.Unused,
                altDesc: (_s = partial.altDesc) !== null && _s !== void 0 ? _s : exports.AltitudeRestrictionType.Unused,
                altitude1: (_t = partial.altitude1) !== null && _t !== void 0 ? _t : 0,
                altitude2: (_u = partial.altitude2) !== null && _u !== void 0 ? _u : 0,
                course: (_v = partial.course) !== null && _v !== void 0 ? _v : 0,
                fixTypeFlags: (_w = partial.fixTypeFlags) !== null && _w !== void 0 ? _w : 0,
                verticalAngle: (_x = partial.verticalAngle) !== null && _x !== void 0 ? _x : 0,
                rnp: (_y = partial.rnp) !== null && _y !== void 0 ? _y : 0,
                lat: partial.lat,
                lon: partial.lon,
            };
        }
        /**
         * Reconciles a pair of associated ICAO properties in a partial `FlightPlanLeg` object. If one of the pair is defined
         * but the other is not, then the undefined property will be added to the object with a value derived from its
         * defined partner. If both of the pair are defined but have incompatible values, then the string (V1) property will
         * be redefined to be compatible with the struct property. Otherwise, the object will remain unchanged.
         * @param leg The object containing the properties to reconcile.
         * @param icaoStructProp The name of the ICAO struct property to reconcile.
         * @param icaoStringProp The name of the ICAO string (V1) property to reconcile.
         */
        static reconcileFlightPlanLegIcaoPropPair(leg, icaoStructProp, icaoStringProp) {
            const isStructPropDefined = leg[icaoStructProp] !== undefined;
            const isStringPropDefined = leg[icaoStringProp] !== undefined;
            if (isStructPropDefined && !isStringPropDefined) {
                // Struct prop is defined but string prop is not -> need to set string prop from struct prop
                leg[icaoStringProp] = ICAO.tryValueToStringV1(leg[icaoStructProp]);
            }
            else if (!isStructPropDefined && isStringPropDefined) {
                // String prop is defined but struct prop is not -> need to set struct prop from string prop
                leg[icaoStructProp] = ICAO.stringV1ToValue(leg[icaoStringProp]);
            }
            else if (isStructPropDefined && isStringPropDefined) {
                const structString = ICAO.tryValueToStringV1(leg[icaoStructProp]);
                if (structString !== leg[icaoStringProp]) {
                    // Both struct and string props are defined but their values are incompatible -> need to set string prop from struct prop
                    leg[icaoStringProp] = structString;
                }
            }
        }
        /**
         * Creates an empty procedure details object.
         * @returns An empty procedure details object.
         */
        static createProcedureDetails() {
            return {
                originRunway: undefined,
                departureFacilityIcaoStruct: undefined,
                departureFacilityIcao: undefined,
                departureIndex: -1,
                departureTransitionIndex: -1,
                departureRunwayIndex: -1,
                arrivalFacilityIcaoStruct: undefined,
                arrivalFacilityIcao: undefined,
                arrivalIndex: -1,
                arrivalTransitionIndex: -1,
                arrivalRunwayTransitionIndex: -1,
                arrivalRunway: undefined,
                approachFacilityIcaoStruct: undefined,
                approachFacilityIcao: undefined,
                approachIndex: -1,
                approachTransitionIndex: -1,
                destinationRunway: undefined,
            };
        }
    }

    /**
     * Manages the active flightplans of the navigational systems.
     */
    class FlightPlanner {
        /**
         * Set a new active plan index.
         * @param planIndex The new active plan index.
         */
        set activePlanIndex(planIndex) {
            this._activePlanIndex = planIndex;
        }
        /**
         * Get the active plan index.
         * @returns The active plan index number.
         */
        get activePlanIndex() {
            return this._activePlanIndex;
        }
        /**
         * Creates an instance of the FlightPlanner.
         * @param id This planner's ID.
         * @param bus The event bus.
         * @param options Options with which to configure the flight planner.
         */
        constructor(id, bus, options) {
            var _a;
            this.id = id;
            this.bus = bus;
            this.eventSuffix = (this.id === '' ? '' : `_${this.id}`);
            this.eventSubscriber = this.bus.getSubscriber();
            /** The flight plans managed by this flight planner. */
            this.flightPlans = [];
            this.ignoreSync = false;
            /** The active flight plan index. */
            this._activePlanIndex = 0;
            this.syncedBatchEntries = [];
            /** Invoked when we receive a flight plan response event. */
            this.flightPlanSynced = new SubEvent();
            this.calculator = options.calculator;
            this.getLegNameFunc = (_a = options === null || options === void 0 ? void 0 : options.getLegName) !== null && _a !== void 0 ? _a : FlightPlanner.buildDefaultLegName;
            this.publisher = bus.getPublisher();
        }
        //PM Modif set the id after building the planner
        setId(id = '') {

            this.id = id;
            this.eventSuffix = (this.id === '' ? '' : `_${this.id}`);

            const subscriber = this.bus.getSubscriber();
            subscriber.on(`fplsync_fplRequest${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onFlightPlanRequest(data));
            subscriber.on(`fplsync_fplResponse${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onFlightPlanResponse(data));
            subscriber.on(`fplsync_fplCreated${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onPlanCreated(data));
            subscriber.on(`fplsync_fplDeleted${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onPlanDeleted(data));
            subscriber.on(`fplsync_fplActiveLegChange${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onActiveLegChanged(data));
            subscriber.on(`fplsync_fplLegChange${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onLegChanged(data));
            subscriber.on(`fplsync_fplSegmentChange${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onSegmentChanged(data));
            subscriber.on(`fplsync_fplOriginDestChanged${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onOriginDestChanged(data));
            subscriber.on(`fplsync_fplProcDetailsChanged${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onProcedureDetailsChanged(data));
            subscriber.on(`fplsync_fplIndexChanged${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onPlanIndexChanged(data));
            subscriber.on(`fplsync_fplUserDataSet${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onUserDataSet(data));
            subscriber.on(`fplsync_fplUserDataDelete${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onUserDataDelete(data));
            subscriber.on(`fplsync_fplLegUserDataSet${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onLegUserDataSet(data));
            subscriber.on(`fplsync_fplLegUserDataDelete${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onLegUserDataDelete(data));
            subscriber.on(`fplsync_fplDirectToDataChanged${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onDirectToDataChanged(data));
            subscriber.on(`fplsync_fplCalculatePended${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onCalculatePended(data));
            subscriber.on(`fplsync_fplCopied${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onPlanCopied(data));
            subscriber.on(`fplsync_fplBatchOpened${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onBatchOpened(data));
            subscriber.on(`fplsync_fplBatchClosed${this.eventSuffix}`).handle(data => !this.ignoreSync && this.onBatchClosed(data));

        }
        //End PM Modif
        /**
         * Gets an event bus subscriber for topics published by this flight planner.
         * @returns An event bus subscriber for topics published by this flight planner.
         */
        getEventSubscriber() {
            return this.eventSubscriber;
        }
        /**
         * Subscribes to one of the event bus topics published by this flight planner.
         * @param baseTopic The base name of the topic to which to subscribe.
         * @returns A consumer for the specified event bus topic.
         */
        onEvent(baseTopic) {
            return this.eventSubscriber.on(`${baseTopic}${this.eventSuffix}`);
        }
        /**
         * Requests synchronization from other FlightPlanner instances.
         */
        requestSync() {
            this.sendFlightPlanRequest();
        }
        /**
         * An event generated when a set of flight plans is requested.
         * @param data The event data.
         */
        onFlightPlanRequest(data) {
            this.ignoreSync = true;
            this.publisher.pub(`fplsync_fplResponse${this.eventSuffix}`, {
                uid: data.uid,
                flightPlans: this.flightPlans.map(plan => {
                    if (!plan) {
                        return null;
                    }
                    const newPlan = Object.assign({}, plan);
                    newPlan.calculator = undefined;
                    return newPlan;
                }), planIndex: this.activePlanIndex
            }, true, false);
            this.ignoreSync = false;
        }
        /**
         * Sends a flight plan request event.
         */
        sendFlightPlanRequest() {
            this.ignoreSync = true;
            this.publisher.pub(`fplsync_fplRequest${this.eventSuffix}`, { uid: this.lastRequestUid = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER) }, true, false);
            this.ignoreSync = false;
        }
        /**
         * A callback which is called in response to flight plan request response sync events.
         * @param data The event data.
         */
        onFlightPlanResponse(data) {
            if (data.uid !== this.lastRequestUid) {
                return;
            }
            this.lastRequestUid = undefined;
            for (let i = 0; i < data.flightPlans.length; i++) {
                const flightPlan = data.flightPlans[i];
                if (flightPlan === null || flightPlan.segmentCount === 0) {
                    continue;
                }
                //PM Modif check flight plan integrity
                let fp = data.flightPlans[i];
                for(let j=0; j<fp.planSegments.length; j++) {
                    if(fp.planSegments[j] == null) {
                        // Remove all segments null and after
                        fp.planSegments.splice(j);
                        break;
                    }
                }
                //End PM Modif
                const newPlan = new FlightPlan(i, this.calculator, this.getLegNameFunc);
                newPlan.copyFrom(flightPlan, true);
                newPlan.events = this.buildPlanEventHandlers(i);
                this.flightPlans[i] = newPlan;
                this.sendEvent('fplLoaded', { planIndex: i }, false);
                // Make sure the newly loaded plans are calculated at least once from the beginning
                newPlan.calculate(0);
            }
            // Only process a plan index changed event if the plan actually exists.
            if (this.flightPlans[data.planIndex]) {
                this.onPlanIndexChanged(data);
            }
            this.flightPlanSynced.notify(this, true);
        }
        /**
         * Checks whether a flight plan exists at a specified index.
         * @param planIndex The index to check.
         * @returns Whether a a flight plan exists at `planIndex`.
         */
        hasFlightPlan(planIndex) {
            return !!this.flightPlans[planIndex];
        }
        /**
         * Gets a flight plan from the flight planner.
         * @param planIndex The index of the flight plan.
         * @returns The requested flight plan.
         * @throws Error if a flight plan does not exist at `planIndex`.
         */
        getFlightPlan(planIndex) {
            const plan = this.flightPlans[planIndex];
            if (!plan) {
                throw new Error(`FlightPlanner: Flight plan does not exist at index ${planIndex}`);
            }
            return plan;
        }
        /**
         * Creates a new flight plan at a specified index if one does not already exist.
         * @param planIndex The index at which to create the new flight plan.
         * @param notify Whether to send an event notification. True by default.
         * @returns The new flight plan, or the existing flight plan at `planIndex`.
         */
        createFlightPlan(planIndex, notify = true) {
            if (this.flightPlans[planIndex]) {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                return this.flightPlans[planIndex];
            }
            const flightPlan = new FlightPlan(planIndex, this.calculator, this.getLegNameFunc);
            flightPlan.events = this.buildPlanEventHandlers(planIndex);
            this.flightPlans[planIndex] = flightPlan;
            notify && this.sendPlanCreated(planIndex);
            return flightPlan;
        }
        /**
         * A callback which is called in response to flight plan request response sync events.
         * @param data The event data.
         */
        onPlanCreated(data) {
            this.createFlightPlan(data.planIndex, false);
            this.sendEvent('fplCreated', data, false);
        }
        /**
         * Sends a flight plan created event.
         * @param planIndex The index of the flight plan that was created.
         */
        sendPlanCreated(planIndex) {
            const data = { planIndex };
            this.sendEvent('fplCreated', data, true);
        }
        /**
         * Deletes a flight plan from the flight planner.
         * @param planIndex The index of the flight plan to delete.
         * @param notify Whether to send an event notification. True by default.
         */
        deleteFlightPlan(planIndex, notify = true) {
            const flightPlan = this.flightPlans[planIndex];
            if (flightPlan) {
                flightPlan.events = {};
                this.flightPlans[planIndex] = undefined;
                this.syncedBatchEntries[planIndex] = undefined;
                notify && this.sendPlanDeleted(planIndex);
            }
            if (planIndex === this.flightPlans.length - 1) {
                this.flightPlans.length--;
            }
        }
        /**
         * A callback which is called in response to flight plan deleted sync events.
         * @param data The event data.
         */
        onPlanDeleted(data) {
            this.deleteFlightPlan(data.planIndex, false);
            this.sendEvent('fplDeleted', data, false);
        }
        /**
         * Sends a flight plan deleted event.
         * @param planIndex The index of the flight plan that was created.
         */
        sendPlanDeleted(planIndex) {
            const data = { planIndex };
            this.sendEvent('fplDeleted', data, true);
        }
        /**
         * Builds the plan event handlers for the flight plan.
         * @param planIndex The index of the flight plan.
         * @returns The plan event handlers.
         */
        buildPlanEventHandlers(planIndex) {
            return {
                onLegChanged: this.sendLegChanged.bind(this, planIndex),
                onSegmentChanged: this.sendSegmentChanged.bind(this, planIndex),
                onActiveLegChanged: this.sendActiveLegChange.bind(this, planIndex),
                onOriginDestChanged: this.sendOriginDestChanged.bind(this, planIndex),
                onProcedureDetailsChanged: this.sendProcedureDetailsChanged.bind(this, planIndex),
                onUserDataSet: this.sendUserDataSet.bind(this, planIndex),
                onUserDataDelete: this.sendUserDataDelete.bind(this, planIndex),
                onLegUserDataSet: this.sendLegUserDataSet.bind(this, planIndex),
                onLegUserDataDelete: this.sendLegUserDataDelete.bind(this, planIndex),
                onDirectDataChanged: this.sendDirectToData.bind(this, planIndex),
                onCalculatePended: this.sendCalculatePended.bind(this, planIndex),
                onCalculated: this.sendCalculated.bind(this, planIndex),
                onBatchOpened: this.sendBatchOpened.bind(this, planIndex),
                onBatchClosed: this.sendBatchClosed.bind(this, planIndex),
                onBatchAsyncClosed: this.sendBatchAsyncClosed.bind(this, planIndex),
            };
        }
        /**
         * Checks whether an active flight plan exists.
         * @returns Whether an active flight plan exists.
         */
        hasActiveFlightPlan() {
            return this.hasFlightPlan(this.activePlanIndex);
        }
        /**
         * Gets the currently active flight plan from the flight planner.
         * @returns The currently active flight plan.
         * @throws Error if no active flight plan exists.
         */
        getActiveFlightPlan() {
            return this.getFlightPlan(this.activePlanIndex);
        }
        /**
         * Copies a flight plan to another flight plan slot.
         * @param sourcePlanIndex The source flight plan index.
         * @param targetPlanIndex The target flight plan index.
         * @param copyCalcs Whether to copy leg calculations (defaults to false).
         * @param notify Whether or not to notify subscribers that the plan has been copied.
         */
        copyFlightPlan(sourcePlanIndex, targetPlanIndex, copyCalcs = false, notify = true) {
            const sourcePlan = this.flightPlans[sourcePlanIndex];
            if (!sourcePlan) {
                return;
            }
            if (this.flightPlans[targetPlanIndex]) {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.flightPlans[targetPlanIndex].copyFrom(sourcePlan, copyCalcs);
            }
            else {
                const newPlan = sourcePlan.copy(targetPlanIndex, copyCalcs);
                newPlan.events = this.buildPlanEventHandlers(targetPlanIndex);
                this.flightPlans[targetPlanIndex] = newPlan;
            }
            if (notify) {
                this.sendPlanCopied(sourcePlanIndex, targetPlanIndex, copyCalcs);
            }
        }
        /**
         * A callback which is called in response to flight plan copied sync events.
         * @param data The event data.
         */
        onPlanCopied(data) {
            this.copyFlightPlan(data.planIndex, data.targetPlanIndex, data.copyCalcs, false);
            this.sendEvent('fplCopied', data, false);
        }
        /**
         * Sends a leg change event.
         * @param planIndex The index of the flight plan that was the source of the copy.
         * @param targetPlanIndex The index of the copy.
         * @param copyCalcs Whether to leg calculations were copied.
         */
        sendPlanCopied(planIndex, targetPlanIndex, copyCalcs) {
            var _a;
            const data = { planIndex, targetPlanIndex, copyCalcs, batch: (_a = this.flightPlans[targetPlanIndex]) === null || _a === void 0 ? void 0 : _a.getBatchStack() };
            this.sendEvent('fplCopied', data, true);
        }
        /**
         * A callback which is called in response to leg changed sync events.
         * @param data The event data.
         */
        onLegChanged(data) {
            const plan = this.getFlightPlan(data.planIndex);
            let localLeg;
            switch (data.type) {
                case exports.LegEventType.Added: {
                    localLeg = plan.addLeg(data.segmentIndex, data.leg.leg, data.legIndex, data.leg.flags, false);
                    break;
                }
                case exports.LegEventType.Removed: {
                    const leg = plan.removeLeg(data.segmentIndex, data.legIndex, false);
                    // We don't want to send the event locally if we didn't find a leg
                    if (!leg) {
                        return;
                    }
                    localLeg = leg;
                    break;
                }
                case exports.LegEventType.Changed: {
                    try {
                        localLeg = plan.getLeg(data.segmentIndex, data.legIndex);
                    }
                    catch (_a) {
                        // We don't want to send the event locally if we didn't find a leg
                        return;
                    }
                    plan.setLegVerticalData(data.segmentIndex, data.legIndex, data.leg.verticalData, false);
                    break;
                }
            }
            // We need to send a reference to the local flight plan's copy of the leg with the local event so that
            // event consumers that save the reference don't become desynced with the local flight plan.
            const localData = {
                planIndex: data.planIndex,
                type: data.type,
                segmentIndex: data.segmentIndex,
                legIndex: data.legIndex,
                leg: localLeg,
                batch: data.batch
            };
            this.sendEvent('fplLegChange', localData, false);
        }
        /**
         * Sends a leg change event.
         * @param planIndex The index of the flight plan.
         * @param segmentIndex The index of the segment.
         * @param index The index of the leg.
         * @param type The type of change.
         * @param leg The leg that was changed.
         * @param batch The modification batch to which the change was assigned.
         */
        sendLegChanged(planIndex, segmentIndex, index, type, leg, batch) {
            const data = {
                planIndex, segmentIndex, legIndex: index, type, leg, batch
            };
            this.sendEvent('fplLegChange', data, true);
        }
        /**
         * A callback which is called in response to segment changed sync events.
         * @param data The event data.
         */
        onSegmentChanged(data) {
            var _a, _b;
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            let localSegment = undefined;
            switch (data.type) {
                case exports.SegmentEventType.Added:
                    localSegment = data.segment && plan.addSegment(data.segmentIndex, data.segment.segmentType, data.segment.airway, false);
                    break;
                case exports.SegmentEventType.Inserted:
                    localSegment = data.segment && plan.insertSegment(data.segmentIndex, data.segment.segmentType, data.segment.airway, false);
                    break;
                case exports.SegmentEventType.Removed:
                    localSegment = (_a = plan.tryGetSegment(data.segmentIndex)) !== null && _a !== void 0 ? _a : undefined;
                    plan.removeSegment(data.segmentIndex, false);
                    break;
                case exports.SegmentEventType.Changed:
                    localSegment = data.segment === undefined ? undefined : (_b = plan.tryGetSegment(data.segmentIndex)) !== null && _b !== void 0 ? _b : undefined;
                    if (localSegment === undefined) {
                        return;
                    }
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    plan.setAirway(data.segmentIndex, data.segment.airway, false);
                    break;
            }
            // We need to send a reference to the local flight plan's copy of the segment with the local event so that
            // event consumers that save the reference don't become desynced with the local flight plan.
            const localData = {
                planIndex: data.planIndex,
                type: data.type,
                segmentIndex: data.segmentIndex,
                segment: localSegment,
                batch: data.batch
            };
            this.sendEvent('fplSegmentChange', localData, false);
        }
        /**
         * Sends a segment change event.
         * @param planIndex The index of the flight plan.
         * @param index The index of the segment.
         * @param type The type of change.
         * @param segment The segment that was changed.
         * @param batch The modification batch to which the change was assigned.
         */
        sendSegmentChanged(planIndex, index, type, segment, batch) {
            const data = {
                planIndex, segmentIndex: index, type, segment, batch
            };
            this.sendEvent('fplSegmentChange', data, true);
        }
        /**
         * A callback which is called in response to active leg changed sync events.
         * @param data The event data.
         */
        onActiveLegChanged(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            switch (data.type) {
                case exports.ActiveLegType.Lateral:
                    plan.setLateralLeg(data.index, false);
                    break;
                case exports.ActiveLegType.Vertical:
                    plan.setVerticalLeg(data.index, false);
                    break;
                case exports.ActiveLegType.Calculating:
                    plan.setCalculatingLeg(data.index, false);
                    break;
            }
            this.sendEvent('fplActiveLegChange', data, false);
        }
        /**
         * Sends an active leg change event.
         * @param planIndex The index of the flight plan.
         * @param index The global index of the leg.
         * @param segmentIndex The index of the plan segment.
         * @param legIndex The index of the leg within the segment.
         * @param previousSegmentIndex The index of the segment in which the previously active leg is.
         * @param previousLegIndex The index of the previously active leg within the previously active segment.
         * @param type The type of leg that was changed.
         * @param batch The modification batch to which the change was assigned.
         */
        sendActiveLegChange(planIndex, index, segmentIndex, legIndex, previousSegmentIndex, previousLegIndex, type, batch) {
            const data = {
                planIndex,
                index,
                segmentIndex, legIndex,
                previousSegmentIndex, previousLegIndex,
                type,
                batch
            };
            this.sendEvent('fplActiveLegChange', data, true);
        }
        /**
         * A callback which is called in response to origin/destination changed sync events.
         * @param data The event data.
         */
        onOriginDestChanged(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            switch (data.type) {
                case exports.OriginDestChangeType.OriginAdded:
                    data.airport && plan.setOriginAirport(data.airport, false);
                    break;
                case exports.OriginDestChangeType.OriginRemoved:
                    plan.removeOriginAirport(false);
                    break;
                case exports.OriginDestChangeType.DestinationAdded:
                    data.airport && plan.setDestinationAirport(data.airport, false);
                    break;
                case exports.OriginDestChangeType.DestinationRemoved:
                    plan.removeDestinationAirport(false);
                    break;
            }
            this.sendEvent('fplOriginDestChanged', data, false);
        }
        /**
         * Sends a origin/dest change event.
         * @param planIndex The index of the flight plan.
         * @param type The origin/destination change type.
         * @param airportIcao The ICAO value of the airport that was changed.
         * @param batch The modification batch to which the change was assigned.
         */
        sendOriginDestChanged(planIndex, type, airportIcao, batch) {
            const data = { planIndex, type, airportIcao, airport: airportIcao ? ICAO.valueToStringV1(airportIcao) : undefined, batch };
            this.sendEvent('fplOriginDestChanged', data, true);
        }
        /**
         * A callback which is called in response to procedure changed sync events.
         * @param data The event data.
         */
        onProcedureDetailsChanged(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            // We do object assign against new proc details in case the incoming details are missing fields because of coming from json
            // and because we want to overwrite the entire object, instead of just some fields.
            plan.setProcedureDetails(Object.assign(FlightPlan.createProcedureDetails(), data.details), false);
            this.sendEvent('fplProcDetailsChanged', data, false);
        }
        /**
         * Sends a procedure details change event.
         * @param planIndex The index of the flight plan.
         * @param details The details that were changed.
         * @param batch The modification batch to which the change was assigned.
         */
        sendProcedureDetailsChanged(planIndex, details, batch) {
            const data = { planIndex, details, batch };
            this.sendEvent('fplProcDetailsChanged', data, true);
        }
        /**
         * A callback which is called in response to flight plan index changed sync events.
         * @param data The event data.
         */
        onPlanIndexChanged(data) {
            this.activePlanIndex = data.planIndex;
            this.sendEvent('fplIndexChanged', data, false);
        }
        /**
         * Sends an active plan index change event.
         * @param planIndex The index of the flight plan.
         */
        sendPlanIndexChanged(planIndex) {
            const data = { planIndex };
            this.sendEvent('fplIndexChanged', data, true);
        }
        /**
         * A callback which is called in response to global user data set sync events.
         * @param data The event data.
         */
        onUserDataSet(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            plan.setUserData(data.key, data.data, false);
            this.sendEvent('fplUserDataSet', data, false);
        }
        /**
         * A callback which is called in response to global user data delete sync events.
         * @param data The event data.
         */
        onUserDataDelete(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            plan.deleteUserData(data.key, false);
            this.sendEvent('fplUserDataDelete', data, false);
        }
        /**
         * Sends a global user data set event.
         * @param planIndex The index of the flight plan.
         * @param key The key of the user data.
         * @param userData The data that was set.
         * @param batch The modification batch to which the change was assigned.
         */
        sendUserDataSet(planIndex, key, userData, batch) {
            const data = { planIndex, key, data: userData, batch };
            this.sendEvent('fplUserDataSet', data, true);
        }
        /**
         * Sends a global user data delete event.
         * @param planIndex The index of the flight plan.
         * @param key The key of the user data.
         * @param batch The modification batch to which the change was assigned.
         */
        sendUserDataDelete(planIndex, key, batch) {
            const data = { planIndex, key, batch };
            this.sendEvent('fplUserDataDelete', data, true);
        }
        /**
         * A callback which is called in response to flight plan leg user data set sync events.
         * @param data The event data.
         */
        onLegUserDataSet(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            const localLeg = plan.tryGetLeg(data.segmentIndex, data.segmentIndex);
            if (!localLeg) {
                return;
            }
            plan.setLegUserData(data.segmentIndex, data.legIndex, data.key, data.data, false);
            // We need to send a reference to the local flight plan's copy of the leg with the local event so that
            // event consumers that save the reference don't become desynced with the local flight plan.
            const localData = {
                planIndex: data.planIndex,
                segmentIndex: data.segmentIndex,
                legIndex: data.legIndex,
                leg: localLeg,
                key: data.key,
                data: data.data,
                batch: data.batch
            };
            this.sendEvent('fplLegUserDataSet', localData, false);
        }
        /**
         * A callback which is called in response to flight plan leg user data delete sync events.
         * @param data The event data.
         */
        onLegUserDataDelete(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            const localLeg = plan.tryGetLeg(data.segmentIndex, data.segmentIndex);
            if (!localLeg) {
                return;
            }
            plan.deleteLegUserData(data.segmentIndex, data.legIndex, data.key, false);
            // We need to send a reference to the local flight plan's copy of the leg with the local event so that
            // event consumers that save the reference don't become desynced with the local flight plan.
            const localData = {
                planIndex: data.planIndex,
                segmentIndex: data.segmentIndex,
                legIndex: data.legIndex,
                leg: localLeg,
                key: data.key,
                batch: data.batch
            };
            this.sendEvent('fplLegUserDataDelete', localData, false);
        }
        /**
         * Sends a flight plan leg user data set event.
         * @param planIndex The index of the flight plan.
         * @param segmentIndex The index of the segment containing the user data's flight plan leg.
         * @param segmentLegIndex The index of the user data's flight plan leg in its containing segment.
         * @param leg The user data's flight plan leg.
         * @param key The key of the user data.
         * @param userData The data that was set.
         * @param batch The modification batch to which the change was assigned.
         */
        sendLegUserDataSet(planIndex, segmentIndex, segmentLegIndex, leg, key, userData, batch) {
            const data = { planIndex, segmentIndex, legIndex: segmentLegIndex, leg, key, data: userData, batch };
            this.sendEvent('fplLegUserDataSet', data, true);
        }
        /**
         * Sends a flight plan leg user data delete event.
         * @param planIndex The index of the flight plan.
         * @param segmentIndex The index of the segment containing the user data's flight plan leg.
         * @param segmentLegIndex The index of the user data's flight plan leg in its containing segment.
         * @param leg The user data's flight plan leg.
         * @param key The key of the user data that was deleted.
         * @param batch The modification batch to which the change was assigned.
         */
        sendLegUserDataDelete(planIndex, segmentIndex, segmentLegIndex, leg, key, batch) {
            const data = { planIndex, segmentIndex, legIndex: segmentLegIndex, leg, key, batch };
            this.sendEvent('fplLegUserDataDelete', data, true);
        }
        /**
         * A callback which is called in response to direct to data changed sync events.
         * @param data The event data.
         */
        onDirectToDataChanged(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            plan.setDirectToData(data.directToData.segmentIndex, data.directToData.segmentLegIndex, false);
            this.sendEvent('fplDirectToDataChanged', data, false);
        }
        /**
         * Sends a direct to data changed event.
         * @param planIndex The index of the flight plan.
         * @param directToData The direct to data.
         * @param batch The modification batch to which the change was assigned.
         */
        sendDirectToData(planIndex, directToData, batch) {
            const data = { planIndex, directToData: directToData, batch };
            this.sendEvent('fplDirectToDataChanged', data, true);
        }
        /**
         * A callback which is called in response to calculation pended sync events.
         * @param data The event data.
         */
        async onCalculatePended(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            const syncedBatchEntries = this.syncedBatchEntries[data.planIndex];
            if (syncedBatchEntries && data.batch) {
                for (let i = data.batch.length - 1; i >= 0; i--) {
                    const batch = data.batch[i];
                    const entry = syncedBatchEntries.get(batch.uuid);
                    if (entry) {
                        entry.pendingCalculateCount++;
                    }
                }
            }
            this.sendEvent('fplCalculatePended', data, false);
            await plan.calculate(data.index, false);
            if (this.flightPlans[data.planIndex] !== plan) {
                return;
            }
            this.sendEvent('fplCalculated', data, false);
            if (syncedBatchEntries && data.batch) {
                for (let i = data.batch.length - 1; i >= 0; i--) {
                    const batch = data.batch[i];
                    const entry = syncedBatchEntries.get(batch.uuid);
                    if (entry) {
                        entry.pendingCalculateCount--;
                        if (entry.isClosed && entry.pendingCalculateCount === 0) {
                            this.sendEvent('fplBatchAsyncClosed', entry.eventData, false);
                            syncedBatchEntries.delete(batch.uuid);
                        }
                    }
                }
            }
        }
        /**
         * Sends a calculate pended event.
         * @param planIndex The index of the flight plan.
         * @param index The global index of the flight plan leg that the path is to be generated from.
         * @param batch The modification batch to which the calculation was assigned.
         */
        sendCalculatePended(planIndex, index, batch) {
            const data = { planIndex, index, batch };
            this.sendEvent('fplCalculatePended', data, true);
        }
        /**
         * Sends a calculated event.
         * @param planIndex The index of the flight plan.
         * @param index The global index of the flight plan leg that the path was generated from.
         * @param batch The modification batch to which the calculation was assigned.
         */
        sendCalculated(planIndex, index, batch) {
            const data = { planIndex, index, batch };
            this.sendEvent('fplCalculated', data, true);
        }
        /**
         * A callback which is called in response to modification batch opened sync events.
         * @param data The event data.
         */
        onBatchOpened(data) {
            var _a;
            var _b, _c;
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            const localData = { planIndex: data.planIndex, isSynced: true, batch: data.batch };
            const entries = (_a = (_b = this.syncedBatchEntries)[_c = data.planIndex]) !== null && _a !== void 0 ? _a : (_b[_c] = new Map());
            entries.set(data.batch.uuid, { batch: data.batch, isClosed: false, pendingCalculateCount: 0, eventData: localData });
            this.sendEvent('fplBatchOpened', localData, false);
        }
        /**
         * Sends a modification batch opened event.
         * @param planIndex The index of the flight plan.
         * @param batch The modification batch that was opened.
         */
        sendBatchOpened(planIndex, batch) {
            const data = { planIndex, isSynced: false, batch };
            this.sendEvent('fplBatchOpened', data, true);
        }
        /**
         * A callback which is called in response to modification batch closed sync events.
         * @param data The event data.
         */
        onBatchClosed(data) {
            const plan = this.flightPlans[data.planIndex];
            if (!plan) {
                return;
            }
            const entries = this.syncedBatchEntries[data.planIndex];
            const entry = entries === null || entries === void 0 ? void 0 : entries.get(data.batch.uuid);
            if (entry) {
                entry.isClosed = true;
                this.sendEvent('fplBatchClosed', entry.eventData, false);
                if (entry.pendingCalculateCount === 0) {
                    this.sendEvent('fplBatchAsyncClosed', entry.eventData, false);
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    entries.delete(data.batch.uuid);
                }
            }
        }
        /**
         * Sends a modification batch closed event.
         * @param planIndex The index of the flight plan.
         * @param batch The modification batch that was closed.
         */
        sendBatchClosed(planIndex, batch) {
            const data = { planIndex, isSynced: false, batch };
            this.sendEvent('fplBatchClosed', data, true);
        }
        /**
         * Sends a modification batch async closed event.
         * @param planIndex The index of the flight plan.
         * @param batch The modification batch that was closed.
         */
        sendBatchAsyncClosed(planIndex, batch) {
            const data = { planIndex, isSynced: false, batch };
            this.sendEvent('fplBatchAsyncClosed', data, false);
        }
        /**
         * Method to set an active flight plan index.
         * @param planIndex The index of the flight plan to make active.
         */
        setActivePlanIndex(planIndex) {
            if (this.hasFlightPlan(planIndex)) {
                this.activePlanIndex = planIndex;
                this.sendPlanIndexChanged(planIndex);
            }
        }
        /**
         * Sends a local event and its sync counterpart.
         * @param topic The topic of the local event.
         * @param data The event data.
         * @param sync Whether to send the sync event.
         */
        sendEvent(topic, data, sync) {
            if (sync) {
                this.ignoreSync = true;
                this.publisher.pub(`fplsync_${topic}${this.eventSuffix}`, data, true, false);
                this.ignoreSync = false;
            }
            this.publisher.pub(`${topic}${this.eventSuffix}`, data, false, false);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static getPlanner(arg1, arg2, arg3) {
            let id;
            let bus;
            let options;
            if (typeof arg1 === 'string') {
                id = arg1;
                bus = arg2;
                options = arg3;
            }
            else {
                id = '';
                bus = arg1;
                options = {
                    calculator: arg2,
                    getLegName: arg3
                };
            }
            let instance = FlightPlanner.instances.get(id);
            if (!instance) {
                instance = new FlightPlanner(id, bus, options);
                FlightPlanner.instances.set(id, instance);
            }
            return instance;
        }
        /**
         * Default Method for leg naming - builds leg names using default nomenclature.
         * @param leg The leg to build a name for.
         * @returns The name of the leg.
         */
        static buildDefaultLegName(leg) {
            let legDistanceNM;
            switch (leg.type) {
                case exports.LegType.CA:
                case exports.LegType.FA:
                case exports.LegType.VA:
                    return `${UnitType.METER.convertTo(leg.altitude1, UnitType.FOOT).toFixed(0)}FT`;
                case exports.LegType.FM:
                case exports.LegType.VM:
                    return 'MANSEQ';
                case exports.LegType.FC:
                    legDistanceNM = Math.round(UnitType.METER.convertTo(leg.distance, UnitType.NMILE));
                    return `D${leg.course.toFixed(0).padStart(3, '0')}${String.fromCharCode(64 + Utils.Clamp(legDistanceNM, 1, 26))}`;
                case exports.LegType.CD:
                case exports.LegType.FD:
                case exports.LegType.VD:
                    legDistanceNM = UnitType.METER.convertTo(leg.distance, UnitType.NMILE);
                    return `${ICAO.getIdent(leg.originIcao)}${legDistanceNM.toFixed(1)}`;
                case exports.LegType.CR:
                case exports.LegType.VR:
                    return `${ICAO.getIdent(leg.originIcao)}${leg.theta.toFixed(0)}`;
                case exports.LegType.CI:
                case exports.LegType.VI:
                    return 'INTRCPT';
                case exports.LegType.PI:
                    return 'PROC. TURN';
                case exports.LegType.HA:
                case exports.LegType.HM:
                case exports.LegType.HF:
                    return 'HOLD';
                default:
                    return ICAO.getIdent(leg.fixIcao);
            }
        }
    }
    FlightPlanner.instances = new Map();

    /**
     * VFR traffic pattern departure types used by {@link FlightPlanRoute}.
     */
    exports.FlightPlanRouteVfrPatternDepartureType = void 0;
    (function (FlightPlanRouteVfrPatternDepartureType) {
        FlightPlanRouteVfrPatternDepartureType["None"] = "";
        FlightPlanRouteVfrPatternDepartureType["InitialClimb"] = "InitialClimb";
        FlightPlanRouteVfrPatternDepartureType["StraightOut"] = "Straight";
        FlightPlanRouteVfrPatternDepartureType["Pattern"] = "Pattern";
        FlightPlanRouteVfrPatternDepartureType["PatternIntoDownwind"] = "PatternDownwind";
        FlightPlanRouteVfrPatternDepartureType["Downwind"] = "Downwind";
        FlightPlanRouteVfrPatternDepartureType["Base"] = "Base";
        FlightPlanRouteVfrPatternDepartureType["BaseIntoPattern"] = "BasePattern";
        FlightPlanRouteVfrPatternDepartureType["Overhead"] = "Overhead";
    })(exports.FlightPlanRouteVfrPatternDepartureType || (exports.FlightPlanRouteVfrPatternDepartureType = {}));
    /**
     * VFR traffic pattern approach types used by {@link FlightPlanRoute}.
     */
    exports.FlightPlanRouteVfrPatternApproachType = void 0;
    (function (FlightPlanRouteVfrPatternApproachType) {
        FlightPlanRouteVfrPatternApproachType["None"] = "";
        FlightPlanRouteVfrPatternApproachType["Final"] = "Final";
        FlightPlanRouteVfrPatternApproachType["LongFinal"] = "LongFinal";
        FlightPlanRouteVfrPatternApproachType["Base"] = "Base";
        FlightPlanRouteVfrPatternApproachType["Downwind"] = "Downwind";
        FlightPlanRouteVfrPatternApproachType["DownwindFromInside"] = "DownwindInside";
        FlightPlanRouteVfrPatternApproachType["Downwind45"] = "Downwind45";
        FlightPlanRouteVfrPatternApproachType["DownwindSecond45"] = "DownwindSec45";
        FlightPlanRouteVfrPatternApproachType["Teardrop"] = "Teardrop";
        FlightPlanRouteVfrPatternApproachType["ReverseTeardrop"] = "RevTeardrop";
    })(exports.FlightPlanRouteVfrPatternApproachType || (exports.FlightPlanRouteVfrPatternApproachType = {}));

    exports.RunwaySurfaceCategory = void 0;
    (function (RunwaySurfaceCategory) {
        RunwaySurfaceCategory[RunwaySurfaceCategory["Unknown"] = 1] = "Unknown";
        RunwaySurfaceCategory[RunwaySurfaceCategory["Hard"] = 2] = "Hard";
        RunwaySurfaceCategory[RunwaySurfaceCategory["Soft"] = 4] = "Soft";
        RunwaySurfaceCategory[RunwaySurfaceCategory["Water"] = 8] = "Water";
    })(exports.RunwaySurfaceCategory || (exports.RunwaySurfaceCategory = {}));
    /**
     * Methods for working with Runways and Runway Designations.
     */
    class RunwayUtils {
        /**
         * Creates an empty runway identifier.
         * @returns An empty runway identifier.
         */
        static emptyIdentifier() {
            return {
                __Type: 'JS_RunwayIdentifier',
                number: '',
                designator: ''
            };
        }
        /**
         * Sets a runway identifier to be empty.
         * @param ident The identifier to set.
         * @returns The specified identifier, after it has been set to be empty.
         */
        static toEmptyIdentifier(ident) {
            ident.number = '';
            ident.designator = '';
            return ident;
        }
        /**
         * Gets the runway identifier that describes a {@link OneWayRunway}.
         * @param runway The runway for which to get an identifier.
         * @param out The object to which to write the results. If not defined, then a new identifier object will be
         * created.
         * @returns The runway identifier that describes the specified runway.
         */
        static getIdentifierFromOneWayRunway(runway, out = RunwayUtils.emptyIdentifier()) {
            out.number = RunwayUtils.getNumberString(runway.direction);
            out.designator = RunwayUtils.getDesignatorLetter(runway.runwayDesignator);
            return out;
        }
        /**
         * Gets the standard string representation for a runway number.
         * @param runwayNumber A runway number.
         * @returns The standard string representation for the specified runway number.
         */
        static getNumberString(runwayNumber) {
            var _a;
            return (_a = RunwayUtils.RUNWAY_NUMBER_STRINGS[runwayNumber]) !== null && _a !== void 0 ? _a : '';
        }
        /**
         * Gets the letter for a runway designator.
         * @param designator A runway designator.
         * @param lowerCase Whether the letter should be lower case. False by default.
         * @returns The letter for the specified runway designator.
         */
        static getDesignatorLetter(designator, lowerCase = false) {
            const letter = RunwayUtils.RUNWAY_DESIGNATOR_LETTERS[designator];
            return lowerCase
                ? letter.toLowerCase()
                : letter;
        }
        /**
         * Creates an empty one-way runway.
         * @returns an empty one-way runway.
         */
        static createEmptyOneWayRunway() {
            return {
                parentRunwayIndex: -1,
                designation: '',
                direction: 36,
                runwayDesignator: RunwayDesignator.RUNWAY_DESIGNATOR_NONE,
                course: 0,
                elevation: 0,
                elevationEnd: 0,
                gradient: 0,
                latitude: 0,
                longitude: 0,
                length: 0,
                width: 0,
                startThresholdLength: 0,
                endThresholdLength: 0,
                surface: exports.RunwaySurfaceType.Concrete,
                lighting: exports.RunwayLightingType.Unknown
            };
        }
        /**
         * Utility method to return all of the one-way runways from a single airport facility
         * @param airport is the Airport Facility to evaluate
         * @returns all of the one-way runways in the airport facility, sorted.
         */
        static getOneWayRunwaysFromAirport(airport) {
            const runways = [];
            airport.runways.map((r, i) => RunwayUtils.getOneWayRunways(r, i)).forEach(d => {
                runways.push(d[0]);
                runways.push(d[1]);
            });
            runways.sort(RunwayUtils.sortRunways);
            return runways;
        }
        /**
         * Utility method to return two one-way runways from a single runway facility
         * @param runway is the AirportRunway object to evaluate
         * @param index is the index of the AirportRunway in the Facility
         * @returns splitRunways array of OneWayRunway objects
         */
        static getOneWayRunways(runway, index) {
            const splitRunways = [];
            const designations = runway.designation.split('-');
            for (let i = 0; i < designations.length; i++) {
                const runwayNumber = parseInt(designations[i]);
                let designator = RunwayDesignator.RUNWAY_DESIGNATOR_NONE;
                let course = 0;
                let thresholdDistanceFromCenter = 0;
                let thresholdElevation = 0;
                let endThresholdElevation = 0;
                let ilsFrequency;
                let startThresholdLength = 0, endThresholdLength = 0;
                if (i === 0) {
                    designator = runway.designatorCharPrimary;
                    course = runway.direction;
                    thresholdDistanceFromCenter = (runway.length / 2) - runway.primaryThresholdLength;
                    thresholdElevation = runway.primaryElevation;
                    endThresholdElevation = runway.secondaryElevation;
                    ilsFrequency = runway.primaryILSFrequency.freqMHz === 0 ? undefined : runway.primaryILSFrequency;
                    startThresholdLength = runway.primaryThresholdLength;
                    endThresholdLength = runway.secondaryThresholdLength;
                }
                else if (i === 1) {
                    designator = runway.designatorCharSecondary;
                    course = NavMath.normalizeHeading(runway.direction + 180);
                    thresholdDistanceFromCenter = (runway.length / 2) - runway.secondaryThresholdLength;
                    thresholdElevation = runway.secondaryElevation;
                    endThresholdElevation = runway.primaryElevation;
                    ilsFrequency = runway.secondaryILSFrequency.freqMHz === 0 ? undefined : runway.secondaryILSFrequency;
                    startThresholdLength = runway.secondaryThresholdLength;
                    endThresholdLength = runway.primaryThresholdLength;
                }
                const designation = RunwayUtils.getRunwayNameString(runwayNumber, designator);
                const coordinates = RunwayUtils.tempGeoPoint
                    .set(runway.latitude, runway.longitude)
                    .offset(course - 180, UnitType.METER.convertTo(thresholdDistanceFromCenter, UnitType.GA_RADIAN));
                splitRunways.push({
                    parentRunwayIndex: index,
                    designation,
                    direction: runwayNumber,
                    runwayDesignator: designator,
                    course,
                    elevation: thresholdElevation,
                    elevationEnd: endThresholdElevation,
                    gradient: (endThresholdElevation - thresholdElevation) / (runway.length - startThresholdLength - endThresholdLength) * 100,
                    latitude: coordinates.lat,
                    longitude: coordinates.lon,
                    ilsFrequency,
                    length: runway.length,
                    width: runway.width,
                    startThresholdLength,
                    endThresholdLength,
                    surface: runway.surface,
                    lighting: runway.lighting
                });
            }
            return splitRunways;
        }
        /**
         * Gets a name for a paired runway. Names are formatted as dash-separated pairs of directional (one-way) runway
         * designations, with optional leading zero padding of the runway numbers. If the specified runway is not paired,
         * then the name will be the designation of the primary runway only.
         * @param runway A paired runway.
         * @param padded Whether the runway numbers should be padded with leading zeroes. Defaults to `true`.
         * @returns The name for the specified paired runway.
         */
        static getRunwayPairNameString(runway, padded = true) {
            const pad = padded ? 2 : 0;
            const dashIndex = runway.designation.search('-');
            const primary = `${(dashIndex < 0 ? runway.designation : runway.designation.substring(0, dashIndex)).padStart(pad, '0')}${RunwayUtils.getDesignatorLetter(runway.designatorCharPrimary)}`;
            const secondary = dashIndex < 0 ? '' : `-${runway.designation.substring(dashIndex + 1).padStart(pad, '0')}${RunwayUtils.getDesignatorLetter(runway.designatorCharSecondary)}`;
            return primary + secondary;
        }
        /**
         * Utility method to return the runway name from the number and designator (L/R/C/W)
         * @param runwayNumber is the integer part of a runway name (18, 26, 27, etc)
         * @param designator is the RunwayDesignator enum for the runway
         * @param padded Whether single-char runways should be 0-padded.
         * @param prefix A prefix to put before the runway name.
         * @returns the runway name string
         */
        static getRunwayNameString(runwayNumber, designator, padded = true, prefix = '') {
            let numberText = `${runwayNumber}`;
            if (padded) {
                numberText = numberText.padStart(2, '0');
            }
            return prefix + numberText + RunwayUtils.getDesignatorLetter(designator);
        }
        /**
         * Gets the primary runway number for a paired runway.
         * @param runway A paired runway.
         * @returns The primary runway number for the specified runway.
         */
        static getRunwayNumberPrimary(runway) {
            const dashIndex = runway.designation.search('-');
            if (dashIndex < 0) {
                return parseInt(runway.designation);
            }
            else {
                return parseInt(runway.designation.substring(0, dashIndex));
            }
        }
        /**
         * Gets the secondary runway number for a paired runway.
         * @param runway A paired runway.
         * @returns The secondary runway number for the specified runway, or `undefined` if the runway has no secondary
         * runway.
         */
        static getRunwayNumberSecondary(runway) {
            const dashIndex = runway.designation.search('-');
            if (dashIndex < 0) {
                return undefined;
            }
            else {
                return parseInt(runway.designation.substring(dashIndex + 1));
            }
        }
        /**
         * Gets a one-way runway from an airport that matches a runway designation by number and designator.
         * @param airport The airport facility in which to search for the match.
         * @param runwayNumber A runway number to match.
         * @param runwayDesignator A runway designator to match.
         * @returns The one-way runway which matches the designation, or undefined if no match could be found.
         */
        static matchOneWayRunway(airport, runwayNumber, runwayDesignator) {
            const length = airport.runways.length;
            for (let r = 0; r < length; r++) {
                const runway = airport.runways[r];
                const designation = runway.designation;
                const primaryRunwayNumber = parseInt(designation.split('-')[0]);
                const secondaryRunwayNumber = parseInt(designation.split('-')[1]);
                if (primaryRunwayNumber === runwayNumber && runway.designatorCharPrimary === runwayDesignator) {
                    const oneWayRunways = RunwayUtils.getOneWayRunways(runway, r);
                    return oneWayRunways[0];
                }
                else if (secondaryRunwayNumber === runwayNumber && runway.designatorCharSecondary === runwayDesignator) {
                    const oneWayRunways = RunwayUtils.getOneWayRunways(runway, r);
                    return oneWayRunways[1];
                }
            }
            return undefined;
        }
        /**
         * Gets a one-way runway from an airport that matches a runway designation string.
         * @param airport The airport facility in which to search for the match.
         * @param designation A runway designation.
         * @returns The one-way runway which matches the designation, or undefined if no match could be found.
         */
        static matchOneWayRunwayFromDesignation(airport, designation) {
            const length = airport.runways.length;
            for (let i = 0; i < length; i++) {
                const match = RunwayUtils.getOneWayRunways(airport.runways[i], i).find((r) => {
                    return (r.designation === designation);
                });
                if (match) {
                    return match;
                }
            }
            return undefined;
        }
        /**
         * Gets a one-way runway from an airport that matches a runway ident.
         * @param airport The airport facility in which to search for the match.
         * @param ident A runway ident.
         * @returns The one-way runway which matches the ident, or undefined if no match could be found.
         */
        static matchOneWayRunwayFromIdent(airport, ident) {
            return RunwayUtils.matchOneWayRunwayFromDesignation(airport, ident.substr(2).trim());
        }
        /**
         * Utility method to return the procedures for a given runway.
         * @param procedures The procedures for the airport.
         * @param runway The given runway to find procedures for.
         * @returns A list of approach procedures for the given runway.
         */
        static getProceduresForRunway(procedures, runway) {
            const oneways = new Array();
            // TODO Make the designation splitting logic a common routine too.
            const designations = runway.designation.split('-');
            for (let i = 0; i < designations.length; i++) {
                const runwayNumber = parseInt(designations[i]);
                let runwayName;
                if (i === 0) {
                    runwayName = RunwayUtils.getRunwayNameString(runwayNumber, runway.designatorCharPrimary, false, '');
                }
                else {
                    runwayName = RunwayUtils.getRunwayNameString(runwayNumber, runway.designatorCharSecondary, false, '');
                }
                oneways.push(runwayName);
            }
            const found = new Array();
            for (const procedure of procedures) {
                if (oneways.includes(procedure.runway.trim())) {
                    found.push(procedure);
                }
                else if (procedure.runwayNumber === 0) {
                    found.push(procedure);
                }
            }
            return found;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static getLocFrequency(airport, arg1, arg2) {
            let runway;
            if (typeof arg1 === 'string') {
                const matchedRunway = RunwayUtils.matchOneWayRunwayFromDesignation(airport, arg1);
                if (!matchedRunway) {
                    return undefined;
                }
                runway = matchedRunway;
            }
            else if (typeof arg1 === 'number') {
                const matchedRunway = RunwayUtils.matchOneWayRunway(airport, arg1, arg2);
                if (!matchedRunway) {
                    return undefined;
                }
                runway = matchedRunway;
            }
            else {
                runway = arg1;
            }
            const runwayDesignation = runway.designation;
            if (runway.ilsFrequency) {
                return runway.ilsFrequency;
            }
            for (let i = 0; i < airport.frequencies.length; i++) {
                // Note: drop the leading zero in the runway designation for the search because some third-party sceneries
                // format the frequency names without the leading zero.
                const match = airport.frequencies[i].name.search(runwayDesignation.replace(/^0/, ''));
                if (match > -1) {
                    return airport.frequencies[i];
                }
            }
            return undefined;
        }
        /**
         * Gets the back course frequency for a runway.
         * @param airport The airport to which the query runway belongs.
         * @param runwayNumber The number of the query runway.
         * @param runwayDesignator The designator of the query runway.
         * @returns The bc frequency for the query runway, or undefined if one could not be found.
         */
        static getBcFrequency(airport, runwayNumber, runwayDesignator) {
            const matchedRunway = RunwayUtils.getOppositeOneWayRunway(airport, runwayNumber, runwayDesignator);
            if (!matchedRunway) {
                return undefined;
            }
            return RunwayUtils.getLocFrequency(airport, matchedRunway);
        }
        /**
         * Get the opposite one way runway from a runway number and designator.
         * @param airport The airport to which the query runway belongs.
         * @param runwayNumber The number of the query runway.
         * @param runwayDesignator The designator of the query runway.
         * @returns The opposite one way runway for the query runway, or undefined if one could not be found.
         */
        static getOppositeOneWayRunway(airport, runwayNumber, runwayDesignator) {
            const oppositeRunwayNumber = Math.round(NavMath.normalizeHeading(10 * (runwayNumber + 18)) / 10);
            let oppositeRunwayDesignator = RunwayDesignator.RUNWAY_DESIGNATOR_NONE;
            switch (runwayDesignator) {
                case RunwayDesignator.RUNWAY_DESIGNATOR_LEFT:
                    oppositeRunwayDesignator = RunwayDesignator.RUNWAY_DESIGNATOR_RIGHT;
                    break;
                case RunwayDesignator.RUNWAY_DESIGNATOR_RIGHT:
                    oppositeRunwayDesignator = RunwayDesignator.RUNWAY_DESIGNATOR_LEFT;
                    break;
                default:
                    oppositeRunwayDesignator = runwayDesignator;
                    break;
            }
            return RunwayUtils.matchOneWayRunway(airport, oppositeRunwayNumber, oppositeRunwayDesignator);
        }
        /**
         * A comparer for sorting runways by number, and then by L, C, and R.
         * @param r1 The first runway to compare.
         * @param r2 The second runway to compare.
         * @returns -1 if the first is before, 0 if equal, 1 if the first is after.
         */
        static sortRunways(r1, r2) {
            if (r1.direction === r2.direction) {
                let v1 = 0;
                if (r1.designation.indexOf('L') != -1) {
                    v1 = 1;
                }
                else if (r1.designation.indexOf('C') != -1) {
                    v1 = 2;
                }
                else if (r1.designation.indexOf('R') != -1) {
                    v1 = 3;
                }
                let v2 = 0;
                if (r2.designation.indexOf('L') != -1) {
                    v2 = 1;
                }
                else if (r2.designation.indexOf('C') != -1) {
                    v2 = 2;
                }
                else if (r2.designation.indexOf('R') != -1) {
                    v2 = 3;
                }
                return v1 - v2;
            }
            return r1.direction - r2.direction;
        }
        /**
         * Gets the ICAO value for the runway facility associated with a one-way runway.
         * @param airport The runway's parent airport, or the ICAO value of the airport.
         * @param runway A one-way runway.
         * @returns The ICAO value for the runway facility associated with the specified one-way runway.
         */
        static getRunwayFacilityIcaoValue(airport, runway) {
            const airportIcao = airport.__Type === 'JS_ICAO'
                ? airport
                : airport.icaoStruct;
            return ICAO.value(exports.IcaoType.Runway, '', airportIcao.ident, `RW${runway.designation}`);
        }
        /**
         * Gets the ICAO string for the runway facility associated with a one-way runway.
         * @param airport The runway's parent airport, or the ICAO of the airport.
         * @param runway A one-way runway.
         * @returns the ICAO string for the runway facility associated with the one-way runway.
         * @deprecated Please use {@link RunwayUtils.getRunwayFacilityIcaoValue | getRunwayFacilityIcaoValue()} instead.
         */
        static getRunwayFacilityIcao(airport, runway) {
            const icao = typeof airport === 'string' ? airport : airport.icao;
            return `R  ${icao.substring(7, 11)}RW${runway.designation.padEnd(3, ' ')}`;
        }
        /**
         * Creates a runway waypoint facility from a runway.
         * @param airport The runway's parent airport.
         * @param runway A one-way runway.
         * @returns A runway waypoint facility corresponding to the runway.
         */
        static createRunwayFacility(airport, runway) {
            const icao = RunwayUtils.getRunwayFacilityIcao(airport, runway);
            return {
                icao,
                icaoStruct: ICAO.stringV1ToValue(icao),
                name: `Runway ${runway.designation}`,
                region: airport.region,
                city: airport.city,
                lat: runway.latitude,
                lon: runway.longitude,
                runway
            };
        }
        /**
         * Gets an alpha code from a runway number.
         * @param number is the runway number.
         * @returns a letter.
         */
        static getRunwayCode(number) {
            const n = Math.round(number);
            return String.fromCharCode(48 + n + (n > 9 ? 7 : 0));
        }
        /**
         * Gets the runway surface category from a runway or runway surface type.
         * @param runway A runway or runway surface type.
         * @returns The surface category of the specified runway or runway surface type.
         */
        static getSurfaceCategory(runway) {
            const surface = typeof runway === 'object' ? runway.surface : runway;
            if (this.SURFACES_HARD.includes(surface)) {
                return exports.RunwaySurfaceCategory.Hard;
            }
            else if (this.SURFACES_SOFT.includes(surface)) {
                return exports.RunwaySurfaceCategory.Soft;
            }
            else if (this.SURFACES_WATER.includes(surface)) {
                return exports.RunwaySurfaceCategory.Water;
            }
            else {
                return exports.RunwaySurfaceCategory.Unknown;
            }
        }
    }
    RunwayUtils.RUNWAY_NUMBER_STRINGS = [
        '', // NONE
        ...ArrayUtils.create(36, index => `${index + 1}`.padStart(2, '0')), // 01-36
        'N',
        'NE',
        'E',
        'SE',
        'S',
        'SW',
        'W',
        'NW',
    ];
    RunwayUtils.RUNWAY_DESIGNATOR_LETTERS = {
        [RunwayDesignator.RUNWAY_DESIGNATOR_NONE]: '',
        [RunwayDesignator.RUNWAY_DESIGNATOR_LEFT]: 'L',
        [RunwayDesignator.RUNWAY_DESIGNATOR_RIGHT]: 'R',
        [RunwayDesignator.RUNWAY_DESIGNATOR_CENTER]: 'C',
        [RunwayDesignator.RUNWAY_DESIGNATOR_WATER]: 'W',
        [RunwayDesignator.RUNWAY_DESIGNATOR_A]: 'A',
        [RunwayDesignator.RUNWAY_DESIGNATOR_B]: 'B',
    };
    RunwayUtils.SURFACES_HARD = [
        exports.RunwaySurfaceType.Asphalt,
        exports.RunwaySurfaceType.Bituminous,
        exports.RunwaySurfaceType.Brick,
        exports.RunwaySurfaceType.Concrete,
        exports.RunwaySurfaceType.Ice,
        exports.RunwaySurfaceType.Macadam,
        exports.RunwaySurfaceType.Paint,
        exports.RunwaySurfaceType.Planks,
        exports.RunwaySurfaceType.SteelMats,
        exports.RunwaySurfaceType.Tarmac,
        exports.RunwaySurfaceType.Urban,
    ];
    RunwayUtils.SURFACES_SOFT = [
        exports.RunwaySurfaceType.Coral,
        exports.RunwaySurfaceType.Dirt,
        exports.RunwaySurfaceType.Forest,
        exports.RunwaySurfaceType.Grass,
        exports.RunwaySurfaceType.GrassBumpy,
        exports.RunwaySurfaceType.Gravel,
        exports.RunwaySurfaceType.HardTurf,
        exports.RunwaySurfaceType.LongGrass,
        exports.RunwaySurfaceType.OilTreated,
        exports.RunwaySurfaceType.Sand,
        exports.RunwaySurfaceType.Shale,
        exports.RunwaySurfaceType.ShortGrass,
        exports.RunwaySurfaceType.Snow,
        exports.RunwaySurfaceType.WrightFlyerTrack
    ];
    RunwayUtils.SURFACES_WATER = [
        exports.RunwaySurfaceType.WaterFSX,
        exports.RunwaySurfaceType.Lake,
        exports.RunwaySurfaceType.Ocean,
        exports.RunwaySurfaceType.Pond,
        exports.RunwaySurfaceType.River,
        exports.RunwaySurfaceType.WasteWater,
        exports.RunwaySurfaceType.Water
    ];
    RunwayUtils.tempGeoPoint = new GeoPoint(0, 0);

    /**
     * A utility class for working with approach procedures.
     */
    class ApproachUtils {
        /**
         * Converts an approach type name to its corresponding approach type.
         * @param name The name to convert.
         * @returns The approach type corresponding to the specified name.
         */
        static nameToType(name) {
            switch (name) {
                case exports.ApproachTypeName.Gps: return ApproachType.APPROACH_TYPE_GPS;
                case exports.ApproachTypeName.Vor: return ApproachType.APPROACH_TYPE_VOR;
                case exports.ApproachTypeName.Ndb: return ApproachType.APPROACH_TYPE_NDB;
                case exports.ApproachTypeName.Ils: return ApproachType.APPROACH_TYPE_ILS;
                case exports.ApproachTypeName.Loc: return ApproachType.APPROACH_TYPE_LOCALIZER;
                case exports.ApproachTypeName.Sdf: return ApproachType.APPROACH_TYPE_SDF;
                case exports.ApproachTypeName.Lda: return ApproachType.APPROACH_TYPE_LDA;
                case exports.ApproachTypeName.VorDme: return ApproachType.APPROACH_TYPE_VORDME;
                case exports.ApproachTypeName.NdbDme: return ApproachType.APPROACH_TYPE_NDBDME;
                case exports.ApproachTypeName.Rnav: return ApproachType.APPROACH_TYPE_RNAV;
                case exports.ApproachTypeName.LocBackcourse: return ApproachType.APPROACH_TYPE_LOCALIZER_BACK_COURSE;
                case exports.ApproachTypeName.GeneratedVisual: return exports.AdditionalApproachType.APPROACH_TYPE_VISUAL;
                default:
                    return ApproachType.APPROACH_TYPE_UNKNOWN;
            }
        }
        /**
         * Converts an approach type to its corresponding approach type name.
         * @param type The type to convert.
         * @returns The approach type name corresponding to the specified type.
         */
        static typeToName(type) {
            switch (type) {
                case ApproachType.APPROACH_TYPE_GPS: return exports.ApproachTypeName.Gps;
                case ApproachType.APPROACH_TYPE_VOR: return exports.ApproachTypeName.Vor;
                case ApproachType.APPROACH_TYPE_NDB: return exports.ApproachTypeName.Ndb;
                case ApproachType.APPROACH_TYPE_ILS: return exports.ApproachTypeName.Ils;
                case ApproachType.APPROACH_TYPE_LOCALIZER: return exports.ApproachTypeName.Loc;
                case ApproachType.APPROACH_TYPE_SDF: return exports.ApproachTypeName.Sdf;
                case ApproachType.APPROACH_TYPE_LDA: return exports.ApproachTypeName.Lda;
                case ApproachType.APPROACH_TYPE_VORDME: return exports.ApproachTypeName.VorDme;
                case ApproachType.APPROACH_TYPE_NDBDME: return exports.ApproachTypeName.NdbDme;
                case ApproachType.APPROACH_TYPE_RNAV: return exports.ApproachTypeName.Rnav;
                case ApproachType.APPROACH_TYPE_LOCALIZER_BACK_COURSE: return exports.ApproachTypeName.LocBackcourse;
                case exports.AdditionalApproachType.APPROACH_TYPE_VISUAL: return exports.ApproachTypeName.GeneratedVisual;
                default:
                    return exports.ApproachTypeName.Undefined;
            }
        }
        /**
         * Creates an empty approach identifier.
         * @returns An empty approach identifier.
         */
        static emptyIdentifier() {
            return {
                __Type: 'JS_ApproachIdentifier',
                type: exports.ApproachTypeName.Undefined,
                runway: RunwayUtils.emptyIdentifier(),
                suffix: ''
            };
        }
        /**
         * Sets an approach identifier to be empty.
         * @param ident The identifier to set.
         * @returns The specified identifier, after it has been set to be empty.
         */
        static toEmptyIdentifier(ident) {
            ident.type = exports.ApproachTypeName.Undefined;
            RunwayUtils.toEmptyIdentifier(ident.runway);
            return ident;
        }
        /**
         * Gets the best RNAV minimum type available for a given approach.
         * @param query The approach to check, or its RNAV type flags.
         * @returns The best RNAV minimum type available for the specified approach.
         */
        static getBestRnavType(query) {
            const rnavTypeFlags = typeof query === 'number' ? query : query.rnavTypeFlags;
            if (rnavTypeFlags & exports.RnavTypeFlags.LPV) {
                return exports.RnavTypeFlags.LPV;
            }
            if (rnavTypeFlags & exports.RnavTypeFlags.LNAVVNAV) {
                return exports.RnavTypeFlags.LNAVVNAV;
            }
            if (rnavTypeFlags & exports.RnavTypeFlags.LP) {
                return exports.RnavTypeFlags.LP;
            }
            if (rnavTypeFlags & exports.RnavTypeFlags.LNAV) {
                return exports.RnavTypeFlags.LNAV;
            }
            return exports.RnavTypeFlags.None;
        }
        /**
         * Checks whether an approach procedure is an RNP (AR) approach.
         * @param approach The approach procedure to check.
         * @returns Whether the approach procedure is an RNP (AR) approach.
         */
        static isRnpAr(approach) {
            return approach.approachType === ApproachType.APPROACH_TYPE_RNAV
                && approach.rnavTypeFlags === exports.RnavTypeFlags.None
                && approach.runwayNumber !== 0;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static getFrequencyFromAirport(facility, approach) {
            if (typeof approach === 'number') {
                approach = facility.approaches[approach];
            }
            if (approach) {
                switch (approach.approachType) {
                    case ApproachType.APPROACH_TYPE_ILS:
                    case ApproachType.APPROACH_TYPE_LOCALIZER:
                    case ApproachType.APPROACH_TYPE_LDA:
                    case ApproachType.APPROACH_TYPE_SDF:
                        return RunwayUtils.getLocFrequency(facility, approach.runwayNumber, approach.runwayDesignator);
                    case ApproachType.APPROACH_TYPE_LOCALIZER_BACK_COURSE:
                        return RunwayUtils.getBcFrequency(facility, approach.runwayNumber, approach.runwayDesignator);
                }
            }
            return undefined;
        }
        /**
         * Gets the origin facility ICAO for the FAF leg of an approach.
         * The facility type is **not** checked against the approach type to ensure it is valid,
         * in contrast to {@link getReferenceFacility} which does perform these checks.
         * @param approach The approach for which to get a reference facility.
         * @returns The ICAO of the origin facility for the FAF leg of the specified approach,
         * or `undefined` if one could not be found.
         */
        static getFafOriginIcao(approach) {
            const finalLegs = approach.finalLegs;
            // Find the faf
            let fafLeg = undefined;
            for (let i = 0; i < finalLegs.length; i++) {
                const leg = finalLegs[i];
                if (BitFlags.isAll(leg.fixTypeFlags, exports.FixTypeFlags.FAF)) {
                    fafLeg = leg;
                    break;
                }
            }
            if (!fafLeg || fafLeg.originIcao === ICAO.emptyIcao) {
                return undefined;
            }
            return fafLeg.originIcao;
        }
        /**
         * Gets the reference facility for an approach. Only ILS, LOC (BC), LDA, SDF, VOR(DME), and NDB(DME) approaches can
         * have reference facilities.
         * @param approach The approach for which to get a reference facility.
         * @param facLoader The facility loader.
         * @returns A Promise which is fulfilled with the reference facility for the specified approach, or `undefined` if
         * one could not be found.
         */
        static async getReferenceFacility(approach, facLoader) {
            let facilityType;
            let isLoc = false;
            switch (approach.approachType) {
                case ApproachType.APPROACH_TYPE_ILS:
                case ApproachType.APPROACH_TYPE_LOCALIZER:
                case ApproachType.APPROACH_TYPE_LOCALIZER_BACK_COURSE:
                case ApproachType.APPROACH_TYPE_LDA:
                case ApproachType.APPROACH_TYPE_SDF:
                    isLoc = true;
                // eslint-disable-next-line no-fallthrough
                case ApproachType.APPROACH_TYPE_VOR:
                case ApproachType.APPROACH_TYPE_VORDME:
                    facilityType = exports.FacilityType.VOR;
                    break;
                case ApproachType.APPROACH_TYPE_NDB:
                case ApproachType.APPROACH_TYPE_NDBDME:
                    facilityType = exports.FacilityType.NDB;
                    break;
                default:
                    return undefined;
            }
            const originIcao = ApproachUtils.getFafOriginIcao(approach);
            if (!originIcao || !ICAO.isFacility(originIcao, facilityType)) {
                return undefined;
            }
            try {
                const facility = await facLoader.getFacility(facilityType, originIcao);
                if (isLoc && facility.type !== exports.VorType.ILS) {
                    return undefined;
                }
                else {
                    return facility;
                }
            }
            catch (_a) {
                return undefined;
            }
        }
    }

    /**
     * A utility class for working with flight plan routes.
     */
    class FlightPlanRouteUtils {
        /**
         * Creates an empty flight plan route.
         * @returns An empty flight plan route.
         */
        static emptyRoute() {
            return {
                __Type: 'JS_FlightPlanRoute',
                departureAirport: ICAO.emptyValue(),
                departureRunway: RunwayUtils.emptyIdentifier(),
                destinationAirport: ICAO.emptyValue(),
                destinationRunway: RunwayUtils.emptyIdentifier(),
                enroute: [],
                departure: '',
                departureTransition: '',
                departureVfrPattern: FlightPlanRouteUtils.emptyVfrPatternDeparture(),
                arrival: '',
                arrivalTransition: '',
                approach: ApproachUtils.emptyIdentifier(),
                approachTransition: '',
                approachVfrPattern: FlightPlanRouteUtils.emptyVfrPatternApproach(),
                cruiseAltitude: null,
                isVfr: false
            };
        }
        /**
         * Sets a flight plan route to be empty.
         * @param route The route to set.
         * @returns The specified flight plan route, after it has been set to be empty.
         */
        static toEmptyRoute(route) {
            route.departureAirport = ICAO.emptyValue();
            RunwayUtils.toEmptyIdentifier(route.departureRunway);
            route.destinationAirport = ICAO.emptyValue();
            RunwayUtils.toEmptyIdentifier(route.destinationRunway);
            route.enroute.length = 0;
            route.departure = '';
            route.departureTransition = '';
            route.arrival = '';
            route.arrivalTransition = '';
            ApproachUtils.toEmptyIdentifier(route.approach);
            route.approachTransition = '';
            route.cruiseAltitude = null;
            return route;
        }
        /**
         * Creates an empty flight plan enroute leg.
         * @returns An empty flight plan enroute leg.
         */
        static emptyEnrouteLeg() {
            return {
                __Type: 'JS_EnrouteLeg',
                fixIcao: ICAO.emptyValue(),
                via: '',
                isPpos: false,
                hasLatLon: false,
                lat: 0,
                lon: 0,
                hasPointBearingDistance: false,
                referenceIcao: ICAO.emptyValue(),
                bearing: 0,
                distance: 0,
                altitude: null,
                name: '',
            };
        }
        /**
         * Sets a flight plan enroute leg to be empty.
         * @param leg The leg to set.
         * @returns The specified leg, after it has been set to be empty.
         */
        static toEmptyEnrouteLeg(leg) {
            leg.fixIcao = ICAO.emptyValue();
            leg.via = '';
            leg.isPpos = false;
            leg.hasLatLon = false;
            leg.lat = 0;
            leg.lon = 0;
            leg.hasPointBearingDistance = false;
            leg.referenceIcao = ICAO.emptyValue();
            leg.bearing = 0;
            leg.distance = 0;
            leg.altitude = null;
            leg.name = '';
            return leg;
        }
        /**
         * Creates an empty flight plan altitude.
         * @returns An empty flight plan altitude.
         */
        static emptyAltitude() {
            return {
                __Type: 'JS_FlightAltitude',
                altitude: 0,
                isFlightLevel: false
            };
        }
        /**
         * Creates an empty VFR traffic pattern departure procedure description.
         * @returns An empty VFR traffic pattern departure procedure description.
         */
        static emptyVfrPatternDeparture() {
            return {
                __Type: 'JS_VfrPatternProcedure',
                type: exports.FlightPlanRouteVfrPatternDepartureType.None,
                isLeftTraffic: true,
                distance: 0,
                altitude: 0
            };
        }
        /**
         * Creates an empty VFR traffic pattern approach procedure description.
         * @returns An empty VFR traffic pattern approach procedure description.
         */
        static emptyVfrPatternApproach() {
            return {
                __Type: 'JS_VfrPatternProcedure',
                type: exports.FlightPlanRouteVfrPatternApproachType.None,
                isLeftTraffic: true,
                distance: 0,
                altitude: 0
            };
        }
        /**
         * Checks whether a flight plan route is empty.
         * @param route The route to check.
         * @param strict Whether to apply a strict check. A strict check considers a route empty if and only if all its
         * fields are themselves empty (except for `isVfr`, which can take any value). A non-strict check considers a route
         * empty if and only if it does not define a departure airport, does not define a destination airport, and defines no
         * enroute legs.
         * @returns Whether the specified flight plan route is empty.
         */
        static isRouteEmpty(route, strict = true) {
            if (!ICAO.isValueEmpty(route.departureAirport)) {
                return false;
            }
            if (!ICAO.isValueEmpty(route.destinationAirport)) {
                return false;
            }
            if (route.enroute.length > 0) {
                return false;
            }
            if (strict) {
                if (route.departureRunway.number !== '' || route.departureRunway.designator !== '') {
                    return false;
                }
                if (route.destinationRunway.number !== '' || route.destinationRunway.designator !== '') {
                    return false;
                }
                if (route.departure !== '' || route.departureTransition !== '') {
                    return false;
                }
                if (route.departureVfrPattern.type !== exports.FlightPlanRouteVfrPatternDepartureType.None
                    || !route.departureVfrPattern.isLeftTraffic
                    || route.departureVfrPattern.distance !== 0
                    || route.departureVfrPattern.altitude !== 0) {
                    return false;
                }
                if (route.arrival !== '' || route.arrivalTransition !== '') {
                    return false;
                }
                if (route.approach.type !== ''
                    || route.approach.runway.number !== ''
                    || route.approach.runway.designator !== ''
                    || route.approach.suffix !== ''
                    || route.approachTransition !== '') {
                    return false;
                }
                if (route.approachVfrPattern.type !== exports.FlightPlanRouteVfrPatternApproachType.None
                    || !route.approachVfrPattern.isLeftTraffic
                    || route.approachVfrPattern.distance !== 0
                    || route.approachVfrPattern.altitude !== 0) {
                    return false;
                }
                if (route.cruiseAltitude !== null) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Checks whether a flight plan route enroute leg is empty.
         * @param leg The leg to check.
         * @returns Whether the specified enroute leg is empty.
         */
        static isEnrouteLegEmpty(leg) {
            return ICAO.isValueEmpty(leg.fixIcao)
                && leg.via === ''
                && leg.isPpos === false
                && leg.hasLatLon === false
                && leg.lat === 0
                && leg.lon === 0
                && leg.hasPointBearingDistance === false
                && ICAO.isValueEmpty(leg.referenceIcao)
                && leg.bearing === 0
                && leg.distance === 0
                && leg.altitude === null
                && leg.name === '';
        }
    }

    /**
     * A manager for working with flight plan routes. The manager can be used to send or retrieve routes. It also
     * automatically publishes the event bus topics defined in {@link FlightPlanRouteEvents} in response to events related
     * to flight plan routes.
     */
    class FlightPlanRouteManager {
        /**
         * Creates a new instance of FlightPlanRouteManager.
         * @param listener A flight plan route view listener.
         */
        constructor(listener) {
            this.listener = listener;
            this.pendingAvionicsRouteRequestsSource = [];
            this._efbRoute = Subject.create(FlightPlanRouteUtils.emptyRoute());
            /** The current EFB flight plan route. */
            this.efbRoute = this._efbRoute;
            this._syncedAvionicsRoute = Subject.create(null);
            /** The most recent synced avionics flight plan route. */
            this.syncedAvionicsRoute = this._syncedAvionicsRoute;
            this._avionicsRouteRequested = new SubEvent();
            /** An event that fires when an avionics route is requested. The event data is the ID of the request. */
            this.avionicsRouteRequested = this._avionicsRouteRequested;
            this._avionicsRouteRequestResponse = new SubEvent();
            /** An event that fires when a response to an avionics route request is received. The event data is the response. */
            this.avionicsRouteRequestResponse = this._avionicsRouteRequestResponse;
            this._pendingAvionicsRouteRequests = Subject.create([]);
            /**
             * An array of the IDs of all avionics route requests that have not received a response yet, in order of decreasing
             * age.
             */
            this.pendingAvionicsRouteRequests = this._pendingAvionicsRouteRequests;
            this.isEfbRouteInit = false;
            listener.on('EfbRouteUpdated', this.onEfbRouteUpdated.bind(this));
            listener.on('AvionicsRouteSync', this.onAvionicsRouteSync.bind(this));
            listener.on('AvionicsRouteRequested', this.onAvionicsRouteRequested.bind(this));
            listener.on('AvionicsRouteRequestResponse', this.onAvionicsRouteRequestResponse.bind(this));
            this.initPromise = this.init();
        }
        /**
         * Gets an instance of {@link FlightPlanRouteManager}.
         * @returns A Promise which is fulfilled with an instance of {@link FlightPlanRouteManager} when the manager is
         * ready.
         */
        static getManager() {
            var _a;
            return (_a = FlightPlanRouteManager.instancePromise) !== null && _a !== void 0 ? _a : (FlightPlanRouteManager.instancePromise = FlightPlanRouteManager.create());
        }
        /**
         * Sends a flight plan route to the EFB.
         * @param route The route to send.
         * @returns A Promise which is fulfilled when the route has been scheduled to be delivered to the EFB. This does
         * not guarantee that the EFB will have received the route when the Promise is fulfilled.
         */
        sendRouteToEfb(route) {
            return this.listener.call('SEND_ROUTE_TO_EFB', route);
        }
        /**
         * Gets the saved EFB route.
         * @returns A Promise which is fulfilled with the saved EFB route once it has been retrieved.
         */
        getEfbRoute() {
            return this.listener.call('GET_EFB_ROUTE');
        }
        /**
         * Saves a route as the EFB route.
         * @param route The route to save.
         * @returns A Promise which is fulfilled when the route has been saved as the EFB route.
         */
        saveEfbRoute(route) {
            return this.listener.call('SAVE_EFB_ROUTE', route);
        }
        /**
         * Sends a flight plan route to avionics for sync.
         * @param route The route to send.
         * @returns A Promise which is fulfilled when the route has been scheduled to be delivered to listeners. This does
         * not guarantee that any listeners will have received the route when the Promise is fulfilled.
         */
        sendRouteToAvionics(route) {
            return this.listener.call('SEND_ROUTE_TO_AVIONICS', route);
        }
        /**
         * Sends an avionics route request to all request listeners.
         * @returns A Promise which is fulfilled with the ID of the request when the request has been scheduled to be
         * delivered to request listeners. This does not guarantee that any listeners will have received the request when the
         * Promise is fulfilled.
         */
        requestAvionicsRoute() {
            return this.listener.call('REQUEST_AVIONICS_ROUTE');
        }
        /**
         * Reply to an avionics route request with a flight plan route.
         * @param requestId The ID of the request to which to reply.
         * @param route The route to send as the reply.
         * @returns A Promise which is fulfilled when the route has been scheduled to be delivered to reply listeners. This
         * does not guarantee that any listeners will have received the reply when the Promise is fulfilled.
         */
        replyToAvionicsRouteRequest(requestId, route) {
            return this.listener.call('REPLY_TO_AVIONICS_ROUTE_REQUEST', route, requestId);
        }
        /**
         * Files a route with the sim's ATC service.
         * @param route The route to file.
         * @returns A Promise which is fulfilled when the route has been scheduled to be delivered to the ATC service. This
         * does not guarantee that the route will have been filed when the Promise is fulfilled.
         */
        fileRouteWithAtc(route) {
            return this.listener.call('FILE_ROUTE_WITH_ATC', route);
        }
        /**
         * Gets the filed ATC route.
         * @returns A Promise which is fulfilled with the filed ATC route once it has been retrieved.
         */
        getAtcRoute() {
            return this.listener.call('GET_ATC_ROUTE');
        }
        /**
         * Initializes this manager.
         */
        async init() {
            await this.initEfbRouteTopic();
        }
        /**
         * Initializes the EFB route topic published to the event bus with the current EFB route.
         */
        async initEfbRouteTopic() {
            const efbRoute = await this.getEfbRoute();
            if (!this.isEfbRouteInit) {
                this.isEfbRouteInit = true;
                this._efbRoute.set(efbRoute);
            }
        }
        /**
         * Responds to when the EFB flight plan route is updated.
         * @param route The updated EFB route.
         */
        onEfbRouteUpdated(route) {
            this.isEfbRouteInit = true;
            this._efbRoute.set(route);
        }
        /**
         * Responds to when a flight plan route is synced to avionics.
         * @param route The route to sync to avionics.
         */
        onAvionicsRouteSync(route) {
            this._syncedAvionicsRoute.set(route);
        }
        /**
         * Responds to when an avionics flight plan route request is received.
         * @param requestId The ID of the request.
         */
        onAvionicsRouteRequested(requestId) {
            let needPublishPendingRequests = false;
            if (!this.pendingAvionicsRouteRequestsSource.includes(requestId)) {
                this.pendingAvionicsRouteRequestsSource.push(requestId);
                needPublishPendingRequests = true;
            }
            this._avionicsRouteRequested.notify(this, requestId);
            if (needPublishPendingRequests) {
                if (!ArrayUtils.equals(this.pendingAvionicsRouteRequestsSource, this._pendingAvionicsRouteRequests.get())) {
                    this._pendingAvionicsRouteRequests.set(Array.from(this.pendingAvionicsRouteRequestsSource));
                }
            }
        }
        /**
         * Responds to when a response to an avionics flight plan route request is received.
         * @param route The flight plan route.
         * @param requestId The ID of the request to which the response is directed.
         */
        onAvionicsRouteRequestResponse(route, requestId) {
            const needPublishPendingRequests = this.pendingAvionicsRouteRequestsSource.splice(this.pendingAvionicsRouteRequestsSource.indexOf(requestId), 1).length > 0;
            this._avionicsRouteRequestResponse.notify(this, { requestId, route });
            if (needPublishPendingRequests) {
                if (!ArrayUtils.equals(this.pendingAvionicsRouteRequestsSource, this._pendingAvionicsRouteRequests.get())) {
                    this._pendingAvionicsRouteRequests.set(Array.from(this.pendingAvionicsRouteRequestsSource));
                }
            }
        }
        /**
         * Creates an instance of FlightPlanRouteManager.
         * @returns A Promise which is fulfilled with a new instance of FlightPlanRouteManager after it has been created.
         */
        static create() {
            return new Promise((resolve, reject) => {
                const gameState = GameStateProvider.get();
                const sub = gameState.sub(state => {
                    if (window['IsDestroying']) {
                        sub.destroy();
                        reject('FlightPlanRouteManager: cannot create a manager after the Coherent JS view has been destroyed');
                        return;
                    }
                    if (state === GameState.briefing || state === GameState.ingame) {
                        sub.destroy();
                        const listener = RegisterViewListener('JS_LISTENER_PLANNEDROUTE', async () => {
                            if (window['IsDestroying']) {
                                reject('FlightPlanRouteManager: cannot create a manager after the Coherent JS view has been destroyed');
                                return;
                            }
                            const manager = new FlightPlanRouteManager(listener);
                            await manager.initPromise;
                            resolve(manager);
                        });
                    }
                }, false, true);
                sub.resume(true);
            });
        }
    }

    /**
     * A Utility Class that supports iterating through a flight plan either forward or reverse.
     */
    class FlightPlanLegIterator {
        constructor() {
            this.cursor = {
                segment: undefined,
                legIndex: -1,
                legDefinition: undefined,
                index: 0
            };
            this.cursorIsBusy = false;
        }
        /**
         * Method that checks whether the FlightPlanLegIterator is busy.
         * @returns Whether the cursor is busy.
         */
        isBusy() {
            return this.cursorIsBusy;
        }
        /**
         * Iterates through the active flight plan in reverse order.
         * @param lateralPlan The lateral flight plan to iterate through.
         * @param each The function to call for each flight plan leg.
         * @throws an Error if the cursor is busy.
         */
        iterateReverse(lateralPlan, each) {
            if (this.cursorIsBusy) {
                throw new Error('FlightPlanLegIterator - iterateReverse: The iterator cursor is busy');
            }
            this.cursorIsBusy = true;
            let segmentIndex = lateralPlan.segmentCount - 1;
            let index = 0;
            try {
                while (segmentIndex >= 0) {
                    const segment = lateralPlan.getSegment(segmentIndex);
                    let legIndex = segment.legs.length - 1;
                    while (legIndex >= 0) {
                        this.cursor.legDefinition = segment.legs[legIndex];
                        this.cursor.legIndex = legIndex;
                        this.cursor.segment = segment;
                        this.cursor.index = index;
                        each(this.cursor);
                        legIndex--;
                        index++;
                    }
                    segmentIndex--;
                }
            }
            catch (error) {
                console.error(`FlightPlanLegIterator - iterateReverse: error in while loop: ${error}`);
                if (error instanceof Error) {
                    console.error(error.stack);
                }
            }
            this.cursorIsBusy = false;
        }
        /**
         * Iterates through the active flight plan in forward order.
         * @param lateralPlan The lateral flight plan to iterate through.
         * @param each The function to call for each flight plan leg.
         * @throws an Error if the cursor is busy.
         */
        iterateForward(lateralPlan, each) {
            if (this.cursorIsBusy) {
                throw new Error('FlightPlanLegIterator - iterateForward: The iterator cursor is busy');
            }
            this.cursorIsBusy = true;
            let segmentIndex = 0;
            let index = 0;
            try {
                while (segmentIndex < lateralPlan.segmentCount) {
                    const segment = lateralPlan.getSegment(segmentIndex);
                    let legIndex = 0;
                    while (legIndex < segment.legs.length) {
                        this.cursor.legDefinition = segment.legs[legIndex];
                        this.cursor.legIndex = legIndex;
                        this.cursor.segment = segment;
                        this.cursor.index = index;
                        each(this.cursor);
                        legIndex++;
                        index++;
                    }
                    segmentIndex++;
                }
            }
            catch (error) {
                console.error(`FlightPlanLegIterator - iterateForward: error in while loop: ${error}`);
                if (error instanceof Error) {
                    console.error(error.stack);
                }
            }
            this.cursorIsBusy = false;
        }
    }

    /**
     * Parts of a flight plan leg path to render.
     */
    exports.FlightPathLegRenderPart = void 0;
    (function (FlightPathLegRenderPart) {
        /** None. */
        FlightPathLegRenderPart[FlightPathLegRenderPart["None"] = 0] = "None";
        /** The ingress transition. */
        FlightPathLegRenderPart[FlightPathLegRenderPart["Ingress"] = 1] = "Ingress";
        /** The base path. */
        FlightPathLegRenderPart[FlightPathLegRenderPart["Base"] = 2] = "Base";
        /** The egress transition. */
        FlightPathLegRenderPart[FlightPathLegRenderPart["Egress"] = 4] = "Egress";
        /** The entire leg path. */
        FlightPathLegRenderPart[FlightPathLegRenderPart["All"] = 7] = "All";
    })(exports.FlightPathLegRenderPart || (exports.FlightPathLegRenderPart = {}));
    /**
     * Renders flight plan leg paths one vector at a time, optionally excluding the ingress and/or egress transition
     * vectors.
     */
    class AbstractFlightPathLegRenderer {
        constructor() {
            this.tempVector = FlightPathUtils.createEmptyVector();
        }
        /**
         * Renders a flight plan leg path to a canvas.
         * @param leg The flight plan leg to render.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream stack to which to render.
         * @param partsToRender The parts of the leg to render, as a combination of {@link FlightPathLegRenderPart}
         * values.
         * @param args Additional arguments.
         */
        render(leg, context, streamStack, partsToRender, ...args) {
            const legCalc = leg.calculated;
            if (!legCalc || !BitFlags.isAny(partsToRender, exports.FlightPathLegRenderPart.Ingress | exports.FlightPathLegRenderPart.Base | exports.FlightPathLegRenderPart.Egress)) {
                return;
            }
            const excludeIngress = !BitFlags.isAll(partsToRender, exports.FlightPathLegRenderPart.Ingress);
            const excludeBase = !BitFlags.isAll(partsToRender, exports.FlightPathLegRenderPart.Base);
            const excludeEgress = !BitFlags.isAll(partsToRender, exports.FlightPathLegRenderPart.Egress);
            let mainVectors = legCalc.ingressToEgress;
            let mainVectorStartIndex = 0;
            let mainVectorEndIndex = legCalc.ingressToEgress.length;
            if (excludeIngress || excludeEgress) {
                mainVectors = legCalc.flightPath;
                mainVectorEndIndex = excludeEgress || legCalc.egressJoinIndex < 0 || legCalc.egress.length === 0 ? legCalc.flightPath.length : legCalc.egressJoinIndex;
            }
            if (!excludeIngress) {
                for (let i = 0; i < legCalc.ingress.length; i++) {
                    this.renderVector(legCalc.ingress[i], true, false, leg, context, streamStack, ...args);
                }
                if (excludeEgress && !excludeBase) {
                    mainVectorStartIndex = Math.max(0, legCalc.ingressJoinIndex);
                    const lastIngressVector = legCalc.ingress[legCalc.ingress.length - 1];
                    const ingressJoinVector = legCalc.flightPath[legCalc.ingressJoinIndex];
                    if (lastIngressVector && ingressJoinVector) {
                        const ingressEnd = AbstractFlightPathLegRenderer.geoPointCache[0].set(lastIngressVector.endLat, lastIngressVector.endLon);
                        const vectorEnd = AbstractFlightPathLegRenderer.geoPointCache[1].set(ingressJoinVector.endLat, ingressJoinVector.endLon);
                        if (!ingressEnd.equals(vectorEnd)) {
                            const ingressJoinVectorCircle = FlightPathUtils.setGeoCircleFromVector(ingressJoinVector, AbstractFlightPathLegRenderer.geoCircleCache[0]);
                            FlightPathUtils.setVectorFromCircle(this.tempVector, ingressJoinVectorCircle, ingressEnd, vectorEnd, ingressJoinVector.flags);
                            this.renderVector(this.tempVector, false, false, leg, context, streamStack, ...args);
                        }
                        mainVectorStartIndex++;
                    }
                }
            }
            if (!excludeBase) {
                const len = Math.min(mainVectorEndIndex, mainVectors.length);
                for (let i = mainVectorStartIndex; i < len; i++) {
                    this.renderVector(mainVectors[i], false, false, leg, context, streamStack, ...args);
                }
            }
            if (!excludeEgress) {
                if (excludeIngress && !excludeBase) {
                    const firstEgressVector = legCalc.egress[0];
                    const egressJoinVector = legCalc.flightPath[legCalc.egressJoinIndex];
                    if (firstEgressVector && egressJoinVector) {
                        const egressStart = AbstractFlightPathLegRenderer.geoPointCache[0].set(firstEgressVector.startLat, firstEgressVector.startLon);
                        const egressJoinVectorStart = AbstractFlightPathLegRenderer.geoPointCache[1].set(egressJoinVector.startLat, egressJoinVector.startLon);
                        if (!egressStart.equals(egressJoinVectorStart)) {
                            const egressJoinVectorCircle = FlightPathUtils.setGeoCircleFromVector(egressJoinVector, AbstractFlightPathLegRenderer.geoCircleCache[0]);
                            FlightPathUtils.setVectorFromCircle(this.tempVector, egressJoinVectorCircle, egressJoinVectorStart, egressStart, egressJoinVector.flags);
                            this.renderVector(this.tempVector, false, false, leg, context, streamStack, ...args);
                        }
                    }
                }
                for (let i = 0; i < legCalc.egress.length; i++) {
                    this.renderVector(legCalc.egress[i], false, true, leg, context, streamStack, ...args);
                }
            }
        }
    }
    AbstractFlightPathLegRenderer.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
    AbstractFlightPathLegRenderer.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /**
     * Renders flight plan paths one leg at a time in either forward or reverse order. Optionally forces the rendering of
     * the active flight plan leg to be last.
     */
    class AbstractFlightPathPlanRenderer {
        /**
         * Constructor.
         * @param renderOrder The order which this renderer renders the flight plan legs. Forward order renders the legs in
         * a first-to-last fashion. Reverse order renders the legs in a last-to-first fashion. Defaults to forward.
         * @param renderActiveLegLast Whether to render the active leg last. Defaults to true.
         */
        constructor(renderOrder = 'forward', renderActiveLegLast = true) {
            this.renderOrder = renderOrder;
            this.renderActiveLegLast = renderActiveLegLast;
        }
        /**
         * Renders a flight plan path to a canvas.
         * @param plan The flight plan to render.
         * @param startIndex The global index of the first flight plan leg to render, inclusive. Defaults to `0`.
         * @param endIndex The global index of the last flight plan leg to render, inclusive. Defaults to `plan.length - 1`.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream stack to which to render.
         * @param args Additional arguments.
         */
        render(plan, startIndex, endIndex, context, streamStack, ...args) {
            startIndex !== null && startIndex !== void 0 ? startIndex : (startIndex = 0);
            endIndex !== null && endIndex !== void 0 ? endIndex : (endIndex = plan.length - 1);
            const activeLegIndex = plan.activeLateralLeg < plan.length ? plan.activeLateralLeg : -1;
            const activeLeg = plan.activeLateralLeg < plan.length ? plan.getLeg(plan.activeLateralLeg) : undefined;
            const isReverse = this.renderOrder === 'reverse';
            if (isReverse) {
                const oldEndIndex = endIndex;
                endIndex = startIndex;
                startIndex = oldEndIndex;
            }
            let index = startIndex;
            const delta = isReverse ? -1 : 1;
            for (const leg of plan.legs(isReverse, startIndex)) {
                if ((index - endIndex) * delta > 0) {
                    break;
                }
                if (this.renderActiveLegLast && index === activeLegIndex) {
                    index += delta;
                    continue;
                }
                this.renderLeg(leg, plan, activeLeg, index, activeLegIndex, context, streamStack, ...args);
                index += delta;
            }
            if (this.renderActiveLegLast && activeLeg) {
                this.renderLeg(activeLeg, plan, activeLeg, activeLegIndex, activeLegIndex, context, streamStack, ...args);
            }
        }
    }

    /**
     * Renders flight plan leg paths one vector at a time, optionally excluding the ingress and/or egress transition
     * vectors. The rendering behavior for each vector is controlled by a function passed to the class constructor.
     */
    class CustomFlightPathLegRenderer extends AbstractFlightPathLegRenderer {
        /**
         * Constructor.
         * @param renderVector A function which renders individual flight path vectors.
         */
        constructor(renderVector) {
            super();
            this.renderVector = renderVector;
        }
    }

    /**
     * An abstract implementation of {@link CssTransform}
     */
    class AbstractCssTransform {
        /**
         * Constructor.
         * @param initialParams The transform's initial parameters.
         */
        constructor(initialParams) {
            this.params = new Float64Array(initialParams);
            this.cachedParams = new Float64Array(initialParams);
        }
        /** @inheritdoc */
        resolve() {
            if (this.stringValue !== undefined && VecNMath.equals(this.params, this.cachedParams)) {
                return this.stringValue;
            }
            VecNMath.copy(this.params, this.cachedParams);
            this.stringValue = this.buildString(this.params);
            return this.stringValue;
        }
    }
    /**
     * A CSS `matrix` transform.
     */
    class CssMatrixTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `matrix` transform, initialized to the identity transformation.
         */
        constructor() {
            super(CssMatrixTransform.DEFAULT_PARAMS);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set(arg1, skewY, skewX, scaleY, translateX, translateY) {
            let scaleX;
            if (typeof arg1 === 'number') {
                scaleX = arg1;
            }
            else {
                [scaleX, skewX, skewY, scaleY, translateX, translateY] = arg1.getParameters();
            }
            this.params[0] = scaleX;
            this.params[1] = skewY;
            this.params[2] = skewX;
            this.params[3] = scaleY;
            this.params[4] = translateX;
            this.params[5] = translateY;
        }
        /** @inheritdoc */
        buildString(params) {
            return `matrix(${params.join(', ')})`;
        }
    }
    CssMatrixTransform.DEFAULT_PARAMS = [1, 0, 0, 1, 0, 0];
    /**
     * A CSS `rotate` transform.
     */
    class CssRotateTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `rotate` transform, initialized to zero rotation.
         * @param unit The angle unit to use for this transform.
         */
        constructor(unit) {
            super(CssRotateTransform.DEFAULT_PARAMS);
            this.unit = unit;
        }
        /**
         * Sets this transform's rotation angle.
         * @param angle The angle to set.
         * @param precision The precision with which to set the angle. A value of `0` denotes infinite precision. Defaults
         * to `0`.
         */
        set(angle, precision = 0) {
            this.params[0] = precision === 0 ? angle : MathUtils.round(angle, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `rotate(${params[0]}${this.unit})`;
        }
    }
    CssRotateTransform.DEFAULT_PARAMS = [0];
    /**
     * A CSS `rotate3d` transform.
     */
    class CssRotate3dTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `rotate3d` transform, initialized to zero rotation about the z axis.
         * @param unit The angle unit to use for this transform.
         */
        constructor(unit) {
            super(CssRotate3dTransform.DEFAULT_PARAMS);
            this.unit = unit;
        }
        /**
         * Sets this transform's rotation.
         * @param x The x component of the rotation axis vector.
         * @param y The y component of the rotation axis vector.
         * @param z The z component of the rotation axis vector.
         * @param angle The rotation angle to set.
         * @param precision The precision with which to set the angle. A value of `0` denotes infinite precision. Defaults
         * to `0`.
         */
        set(x, y, z, angle, precision = 0) {
            this.params[0] = x;
            this.params[1] = y;
            this.params[2] = z;
            this.params[3] = precision === 0 ? angle : MathUtils.round(angle, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `rotate3d(${params[0]}, ${params[1]}, ${params[2]}, ${params[3]}${this.unit})`;
        }
    }
    CssRotate3dTransform.DEFAULT_PARAMS = [0, 0, 1, 0];
    /**
     * A CSS `translateX` transform.
     */
    class CssTranslateXTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `translateX` transform, initialized to zero translation.
         * @param unit The unit to use for this transform.
         */
        constructor(unit) {
            super(CssTranslateXTransform.DEFAULT_PARAMS);
            this.unit = unit;
        }
        /**
         * Sets this transform's translation.
         * @param x The translation to set.
         * @param precision The precision with which to set the translation. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         */
        set(x, precision = 0) {
            this.params[0] = precision === 0 ? x : MathUtils.round(x, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `translateX(${params[0]}${this.unit})`;
        }
    }
    CssTranslateXTransform.DEFAULT_PARAMS = [0];
    /**
     * A CSS `translateY` transform.
     */
    class CssTranslateYTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `translateY` transform, initialized to zero translation.
         * @param unit The unit to use for this transform.
         */
        constructor(unit) {
            super(CssTranslateYTransform.DEFAULT_PARAMS);
            this.unit = unit;
        }
        /**
         * Sets this transform's translation.
         * @param y The translation to set.
         * @param precision The precision with which to set the translation. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         */
        set(y, precision = 0) {
            this.params[0] = precision === 0 ? y : MathUtils.round(y, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `translateY(${params[0]}${this.unit})`;
        }
    }
    CssTranslateYTransform.DEFAULT_PARAMS = [0];
    /**
     * A CSS `translateZ` transform.
     */
    class CssTranslateZTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `translateZ` transform, initialized to zero translation.
         * @param unit The unit to use for this transform.
         */
        constructor(unit) {
            super(CssTranslateZTransform.DEFAULT_PARAMS);
            this.unit = unit;
        }
        /**
         * Sets this transform's translation.
         * @param z The translation to set.
         * @param precision The precision with which to set the translation. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         */
        set(z, precision = 0) {
            this.params[0] = precision === 0 ? z : MathUtils.round(z, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `translateZ(${params[0]}${this.unit})`;
        }
    }
    CssTranslateZTransform.DEFAULT_PARAMS = [0];
    /**
     * A CSS `translate` transform.
     */
    class CssTranslateTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `translate` transform, initialized to zero translation.
         * @param unitX The unit to use for this transform's x translation.
         * @param unitY The unit to use for this transform's y translation. Defaults to the same unit as the x translation.
         */
        constructor(unitX, unitY = unitX) {
            super(CssTranslateTransform.DEFAULT_PARAMS);
            this.unitX = unitX;
            this.unitY = unitY;
        }
        /**
         * Sets this transform's translation.
         * @param x The x translation to set.
         * @param y The y translation to set.
         * @param precisionX The precision with which to set the x translation. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         * @param precisionY The precision with which to set the y translation. A value of `0` denotes infinite precision.
         * Defaults to the x translation precision value.
         */
        set(x, y, precisionX = 0, precisionY = precisionX) {
            this.params[0] = precisionX === 0 ? x : MathUtils.round(x, precisionX);
            this.params[1] = precisionY === 0 ? y : MathUtils.round(y, precisionY);
        }
        /** @inheritdoc */
        buildString(params) {
            return `translate(${params[0]}${this.unitX}, ${params[1]}${this.unitY})`;
        }
    }
    CssTranslateTransform.DEFAULT_PARAMS = [0, 0];
    /**
     * A CSS `translate3d` transform.
     */
    class CssTranslate3dTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `translate3d` transform, initialized to zero translation.
         * @param unitX The unit to use for this transform's x translation.
         * @param unitY The unit to use for this transform's y translation. Defaults to the same unit as the x translation.
         * @param unitZ The unit to use for this transform's z translation. Defaults to the same unit as the x translation.
         */
        constructor(unitX, unitY = unitX, unitZ = unitX) {
            super(CssTranslate3dTransform.DEFAULT_PARAMS);
            this.unitX = unitX;
            this.unitY = unitY;
            this.unitZ = unitZ;
        }
        /**
         * Sets this transform's translation.
         * @param x The x translation to set.
         * @param y The y translation to set.
         * @param z The z translation to set.
         * @param precisionX The precision with which to set the x translation. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         * @param precisionY The precision with which to set the y translation. A value of `0` denotes infinite precision.
         * Defaults to the x translation precision value.
         * @param precisionZ The precision with which to set the z translation. A value of `0` denotes infinite precision.
         * Defaults to the x translation precision value.
         */
        set(x, y, z, precisionX = 0, precisionY = precisionX, precisionZ = precisionX) {
            this.params[0] = precisionX === 0 ? x : MathUtils.round(x, precisionX);
            this.params[1] = precisionY === 0 ? y : MathUtils.round(y, precisionY);
            this.params[2] = precisionZ === 0 ? z : MathUtils.round(z, precisionZ);
        }
        /** @inheritdoc */
        buildString(params) {
            return `translate3d(${params[0]}${this.unitX}, ${params[1]}${this.unitY}, ${params[2]}${this.unitZ})`;
        }
    }
    CssTranslate3dTransform.DEFAULT_PARAMS = [0, 0, 0];
    /**
     * A CSS `scaleX` transform.
     */
    class CssScaleXTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `scaleX` transform, initialized to the identity scaling.
         */
        constructor() {
            super(CssScaleXTransform.DEFAULT_PARAMS);
        }
        /**
         * Sets this transform's scaling.
         * @param x The scaling to set.
         * @param precision The precision with which to set the scaling. A value of `0` denotes infinite precision. Defaults
         * to `0`.
         */
        set(x, precision = 0) {
            this.params[0] = precision === 0 ? x : MathUtils.round(x, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `scaleX(${params[0]})`;
        }
    }
    CssScaleXTransform.DEFAULT_PARAMS = [1];
    /**
     * A CSS `scaleY` transform.
     */
    class CssScaleYTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `scaleY` transform, initialized to the identity scaling.
         */
        constructor() {
            super(CssScaleYTransform.DEFAULT_PARAMS);
        }
        /**
         * Sets this transform's scaling.
         * @param y The scaling to set.
         * @param precision The precision with which to set the scaling. A value of `0` denotes infinite precision. Defaults
         * to `0`.
         */
        set(y, precision = 0) {
            this.params[0] = precision === 0 ? y : MathUtils.round(y, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `scaleY(${params[0]})`;
        }
    }
    CssScaleYTransform.DEFAULT_PARAMS = [1];
    /**
     * A CSS `scaleZ` transform.
     */
    class CssScaleZTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `scaleZ` transform, initialized to the identity scaling.
         */
        constructor() {
            super(CssScaleZTransform.DEFAULT_PARAMS);
        }
        /**
         * Sets this transform's scaling.
         * @param z The scaling to set.
         * @param precision The precision with which to set the scaling. A value of `0` denotes infinite precision. Defaults
         * to `0`.
         */
        set(z, precision = 0) {
            this.params[0] = precision === 0 ? z : MathUtils.round(z, precision);
        }
        /** @inheritdoc */
        buildString(params) {
            return `scaleZ(${params[0]})`;
        }
    }
    CssScaleZTransform.DEFAULT_PARAMS = [1];
    /**
     * A CSS `scale` transform.
     */
    class CssScaleTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `scale` transform, initialized to the identity scaling.
         */
        constructor() {
            super(CssScaleTransform.DEFAULT_PARAMS);
        }
        /**
         * Sets this transform's scaling.
         * @param x The x scaling to set.
         * @param y The y scaling to set.
         * @param precisionX The precision with which to set the x scaling. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         * @param precisionY The precision with which to set the y scaling. A value of `0` denotes infinite precision.
         * Defaults to the x scaling precision value.
         */
        set(x, y, precisionX = 0, precisionY = precisionX) {
            this.params[0] = precisionX === 0 ? x : MathUtils.round(x, precisionX);
            this.params[1] = precisionY === 0 ? y : MathUtils.round(y, precisionY);
        }
        /** @inheritdoc */
        buildString(params) {
            return `scale(${params[0]}, ${params[1]})`;
        }
    }
    CssScaleTransform.DEFAULT_PARAMS = [1, 1];
    /**
     * A CSS `scale3d` transform.
     */
    class CssScale3dTransform extends AbstractCssTransform {
        /**
         * Creates a new instance of a CSS `scale3d` transform, initialized to the identity scaling.
         */
        constructor() {
            super(CssScale3dTransform.DEFAULT_PARAMS);
        }
        /**
         * Sets this transform's scaling.
         * @param x The x scaling to set.
         * @param y The y scaling to set.
         * @param z The z scaling to set.
         * @param precisionX The precision with which to set the x scaling. A value of `0` denotes infinite precision.
         * Defaults to `0`.
         * @param precisionY The precision with which to set the y scaling. A value of `0` denotes infinite precision.
         * Defaults to the x scaling precision value.
         * @param precisionZ The precision with which to set the z scaling. A value of `0` denotes infinite precision.
         * Defaults to the x scaling precision value.
         */
        set(x, y, z, precisionX = 0, precisionY = precisionX, precisionZ = precisionX) {
            this.params[0] = precisionX === 0 ? x : MathUtils.round(x, precisionX);
            this.params[1] = precisionY === 0 ? y : MathUtils.round(y, precisionY);
            this.params[2] = precisionZ === 0 ? z : MathUtils.round(y, precisionZ);
        }
        /** @inheritdoc */
        buildString(params) {
            return `scale3d(${params[0]}, ${params[1]}, ${params[2]})`;
        }
    }
    CssScale3dTransform.DEFAULT_PARAMS = [1, 1, 1];
    /**
     * A concatenated chain of CSS transforms.
     */
    class CssTransformChain {
        /**
         * Creates a new chain of CSS transforms.
         * @param transforms The individual child transforms that will constitute the new transform chain. The order of
         * the children passed to the constructor determines the order of concatenation. Concatenation follows the standard
         * CSS transform convention: for a concatenation of transforms `[A, B, C]`, the resulting transformation is
         * equivalent to the one produced by multiplying the transformation matrices in the order `(A * B) * C`.
         */
        constructor(...transforms) {
            this.stringValues = [];
            this.transforms = transforms;
        }
        /**
         * Gets one of this chain's child transforms.
         * @param index The index of the child to get.
         * @returns The child transform at the specified index in this chain.
         * @throws RangeError if `index` is out of bounds.
         */
        getChild(index) {
            if (index < 0 || index >= this.transforms.length) {
                throw new RangeError();
            }
            return this.transforms[index];
        }
        /** @inheritdoc */
        resolve() {
            let needRebuildString = false;
            for (let i = 0; i < this.transforms.length; i++) {
                const stringValue = this.transforms[i].resolve();
                if (this.stringValues[i] !== stringValue) {
                    this.stringValues[i] = stringValue;
                    needRebuildString = true;
                }
            }
            if (needRebuildString || this.chainedStringValue === undefined) {
                this.chainedStringValue = this.stringValues.join(' ');
            }
            return this.chainedStringValue;
        }
    }
    /**
     * A subscribable subject whose value is a CSS transform string resolved from a {@link CssTransform}.
     */
    class CssTransformSubject extends AbstractSubscribable {
        /**
         * Constructor.
         * @param transform The new subject's CSS transform.
         */
        constructor(transform) {
            super();
            this._transform = transform;
            this.stringValue = transform.resolve();
            this.transform = transform;
        }
        /** @inheritdoc */
        get() {
            return this.stringValue;
        }
        /**
         * Resolves this subject's CSS transform to a CSS transform string, and sets this subject's value to the resolved
         * string. If this changes this subject's value, subscribers will be notified.
         */
        resolve() {
            const stringValue = this._transform.resolve();
            if (stringValue !== this.stringValue) {
                this.stringValue = stringValue;
                this.notify();
            }
        }
        /**
         * Creates a new instance of {@link CssTransformSubject} whose value is resolved from a CSS transform.
         * @param transform A CSS transform.
         * @returns A new instance of {@link CssTransformSubject} whose value is resolved from the specified CSS transform.
         */
        static create(transform) {
            return new CssTransformSubject(transform);
        }
    }
    /**
     * A utility class for building CSS transforms.
     */
    class CssTransformBuilder {
        /**
         * Creates a new instance of a CSS `matrix` transform, initialized to the identity transformation.
         * @returns A new instance of a CSS `matrix` transform, initialized to the identity transformation.
         */
        static matrix() {
            return new CssMatrixTransform();
        }
        /**
         * Creates a new instance of a CSS `rotate` transform, initialized to zero rotation.
         * @param unit The angle unit to use for the new transform.
         * @returns A new instance of a CSS `rotate` transform, initialized to zero rotation.
         */
        static rotate(unit) {
            return new CssRotateTransform(unit);
        }
        /**
         * Creates a new instance of a CSS `rotate3d` transform, initialized to zero rotation about the z axis.
         * @param unit The angle unit to use for the new transform.
         * @returns A new instance of a CSS `rotate3d` transform, initialized to zero rotation about the z axis.
         */
        static rotate3d(unit) {
            return new CssRotate3dTransform(unit);
        }
        /**
         * Creates a new instance of a CSS `translateX` transform, initialized to zero translation.
         * @param unit The unit to use for the new transform.
         * @returns A new instance of a CSS `translateX` transform, initialized to zero translation.
         */
        static translateX(unit) {
            return new CssTranslateXTransform(unit);
        }
        /**
         * Creates a new instance of a CSS `translateY` transform, initialized to zero translation.
         * @param unit The unit to use for the new transform.
         * @returns A new instance of a CSS `translateY` transform, initialized to zero translation.
         */
        static translateY(unit) {
            return new CssTranslateYTransform(unit);
        }
        /**
         * Creates a new instance of a CSS `translateZ` transform, initialized to zero translation.
         * @param unit The unit to use for the new transform.
         * @returns A new instance of a CSS `translateZ` transform, initialized to zero translation.
         */
        static translateZ(unit) {
            return new CssTranslateZTransform(unit);
        }
        /**
         * Creates a new instance of a CSS `translate` transform, initialized to zero translation.
         * @param unitX The unit to use for the new transform's x translation.
         * @param unitY The unit to use for the new transform's y translation.
         * @returns A new instance of a CSS `translate` transform, initialized to zero translation.
         */
        static translate(unitX, unitY) {
            return new CssTranslateTransform(unitX, unitY);
        }
        /**
         * Creates a new instance of a CSS `translate3d` transform, initialized to zero translation.
         * @param unitX The unit to use for the new transform's x translation.
         * @param unitY The unit to use for the new transform's y translation.
         * @param unitZ The unit to use for the new transform's z translation.
         * @returns A new instance of a CSS `translate3d` transform, initialized to zero translation.
         */
        static translate3d(unitX, unitY, unitZ) {
            return new CssTranslate3dTransform(unitX, unitY, unitZ);
        }
        /**
         * Creates a new instance of a CSS `scaleX` transform, initialized to the identity scaling.
         * @returns A new instance of a CSS `scaleX` transform, initialized to the identity scaling.
         */
        static scaleX() {
            return new CssScaleXTransform();
        }
        /**
         * Creates a new instance of a CSS `scaleY` transform, initialized to the identity scaling.
         * @returns A new instance of a CSS `scaleY` transform, initialized to the identity scaling.
         */
        static scaleY() {
            return new CssScaleYTransform();
        }
        /**
         * Creates a new instance of a CSS `scaleZ` transform, initialized to the identity scaling.
         * @returns A new instance of a CSS `scaleZ` transform, initialized to the identity scaling.
         */
        static scaleZ() {
            return new CssScaleZTransform();
        }
        /**
         * Creates a new instance of a CSS `scale` transform, initialized to the identity scaling.
         * @returns A new instance of a CSS `scale` transform, initialized to the identity scaling.
         */
        static scale() {
            return new CssScaleTransform();
        }
        /**
         * Creates a new instance of a CSS `scale3d` transform, initialized to the identity scaling.
         * @returns A new instance of a CSS `scale3d` transform, initialized to the identity scaling.
         */
        static scale3d() {
            return new CssScale3dTransform();
        }
        /**
         * Concatenates zero or more CSS transformations.
         * @param transforms The individual transforms to concatentate. The order of the transforms passed to the function
         * determines the order of concatenation. Concatenation follows the standard CSS transform convention: for a
         * concatenation of transforms `[A, B, C]`, the resulting transformation is equivalent to the one produced by
         * multiplying the transformation matrices in the order `(A * B) * C`.
         * @returns A new {@link CssTransformChain} object representing the concatenation of the specified transforms.
         */
        static concat(...transforms) {
            return new CssTransformChain(...transforms);
        }
    }

    /**
     * A default implementation of {@link MapLabeledRingLabel}.
     */
    class DefaultMapLabeledRingLabel extends DisplayComponent {
        constructor() {
            super(...arguments);
            this.translate = CssTransformSubject.create(CssTransformBuilder.translate('%'));
            this.left = Subject.create('');
            this.top = Subject.create('');
            this.center = new Float64Array(2);
            this.radius = 0;
            this.anchor = new Float64Array(2);
            this.radialAngle = 0;
            this.radialOffset = 0;
        }
        /** @inheritdoc */
        get content() {
            if (this._content === undefined || this._content === null) {
                throw new Error('DefaultMapLabeledRingLabel: unable to access content');
            }
            return this._content;
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            var _a;
            this.thisNode = thisNode;
            this._content = (_a = thisNode.children) === null || _a === void 0 ? void 0 : _a[0].instance;
        }
        /** @inheritdoc */
        getAnchor() {
            return this.anchor;
        }
        /** @inheritdoc */
        getRadialAngle() {
            return this.radialAngle;
        }
        /** @inheritdoc */
        getRadialOffset() {
            return this.radialOffset;
        }
        /** @inheritdoc */
        setAnchor(anchor) {
            this.anchor.set(anchor);
            this.translate.transform.set(-anchor[0] * 100, -anchor[1] * 100);
            this.translate.resolve();
        }
        /** @inheritdoc */
        setRadialAngle(angle) {
            if (this.radialAngle === angle) {
                return;
            }
            this.radialAngle = angle;
            this.updatePosition();
        }
        /** @inheritdoc */
        setRadialOffset(offset) {
            if (this.radialOffset === offset) {
                return;
            }
            this.radialOffset = offset;
            this.updatePosition();
        }
        /**
         * Updates this label with the center and radius of its parent ring.
         * @param center The center of the ring, in pixels.
         * @param radius The radius of the ring, in pixels.
         */
        setRingPosition(center, radius) {
            if (Vec2Math.equals(this.center, center) && radius === this.radius) {
                return;
            }
            this.center.set(center);
            this.radius = radius;
            this.updatePosition();
        }
        /**
         * Updates this label's position.
         */
        updatePosition() {
            const pos = DefaultMapLabeledRingLabel.tempVec2_1;
            Vec2Math.setFromPolar(this.radius + this.radialOffset, this.radialAngle, pos);
            Vec2Math.add(this.center, pos, pos);
            this.left.set(`${pos[0]}px`);
            this.top.set(`${pos[1]}px`);
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent("div", { style: {
                    'position': 'absolute',
                    'left': this.left,
                    'top': this.top,
                    'transform': this.translate
                } }, this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            this.thisNode && exports.FSComponent.shallowDestroy(this.thisNode);
            super.destroy();
        }
    }
    DefaultMapLabeledRingLabel.tempVec2_1 = new Float64Array(2);

    /**
     * Renders arcs along geo circles to a path stream stack.
     */
    class GeoCirclePathRenderer {
        /**
         * Renders an arc along a geo circle to a path stream stack.
         * @param circle The geo circle containing the arc to render.
         * @param startLat The latitude of the start of the arc, in degrees.
         * @param startLon The longitude of the start of the arc, in degrees.
         * @param endLat The latitude of the end of the arc, in degrees.
         * @param endLon The longitude of the end of the arc, in degrees.
         * @param streamStack The path stream stack to which to render.
         * @param continuePath Whether to continue the previously rendered path. If true, a discontinuity in the rendered
         * path will not be inserted before the arc is rendered. This may lead to undesired artifacts if the previously
         * rendered path does not terminate at the point where the projected arc starts. Defaults to false.
         */
        render(circle, startLat, startLon, endLat, endLon, streamStack, continuePath = false) {
            if (!continuePath) {
                streamStack.beginPath();
                streamStack.moveTo(startLon, startLat);
            }
            if (circle.isGreatCircle()) {
                const startPoint = GeoPoint.sphericalToCartesian(startLat, startLon, GeoCirclePathRenderer.vec3Cache[0]);
                const distance = circle.distanceAlong(startPoint, GeoCirclePathRenderer.geoPointCache[0].set(endLat, endLon), Math.PI);
                if (distance >= Math.PI - GeoPoint.EQUALITY_TOLERANCE) {
                    const midPoint = circle.offsetDistanceAlong(startPoint, distance / 2, GeoCirclePathRenderer.geoPointCache[0], Math.PI);
                    const midLat = midPoint.lat;
                    const midLon = midPoint.lon;
                    streamStack.lineTo(midLon, midLat);
                    streamStack.lineTo(endLon, endLat);
                }
                else {
                    streamStack.lineTo(endLon, endLat);
                }
            }
            else {
                const turnCenter = FlightPathUtils.getTurnCenterFromCircle(circle, GeoCirclePathRenderer.geoPointCache[0]);
                const turnDirection = FlightPathUtils.getTurnDirectionFromCircle(circle);
                const isCenterPole = Math.abs(turnCenter.lat) >= 90 - GeoCircle.ANGULAR_TOLERANCE * Avionics.Utils.RAD2DEG;
                let startAngle, endAngle;
                if (isCenterPole) {
                    startAngle = startLon;
                    endAngle = endLon;
                }
                else {
                    startAngle = turnCenter.bearingTo(startLat, startLon);
                    endAngle = turnCenter.bearingTo(endLat, endLon);
                }
                streamStack.arc(turnCenter.lon, turnCenter.lat, FlightPathUtils.getTurnRadiusFromCircle(circle), startAngle, endAngle, turnDirection === 'left');
            }
        }
    }
    GeoCirclePathRenderer.NORTH_POLE_VEC = new Float64Array([0, 0, 1]);
    GeoCirclePathRenderer.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
    GeoCirclePathRenderer.vec3Cache = [new Float64Array(3)];

    /**
     * Renders flight plan leg paths as lines, with support for different styles for each flight path vector in the leg.
     */
    class FlightPathLegLineRenderer extends AbstractFlightPathLegRenderer {
        /**
         * Constructor.
         * @param styleSelector A function which selects a style for each rendered vector.
         */
        constructor(styleSelector) {
            super();
            this.styleSelector = styleSelector;
            this.pathRenderer = new GeoCirclePathRenderer();
            this.styleBuffer = [
                {
                    strokeWidth: 1,
                    strokeStyle: 'white',
                    strokeDash: null,
                    outlineWidth: 0,
                    outlineStyle: 'black',
                    outlineDash: null,
                    isContinuous: false
                },
                {
                    strokeWidth: 1,
                    strokeStyle: 'white',
                    strokeDash: null,
                    outlineWidth: 0,
                    outlineStyle: 'black',
                    outlineDash: null,
                    isContinuous: false
                }
            ];
            this.activeStyleIndex = 0;
            this.isAtLegStart = false;
            this.needStrokeLineAtLegEnd = false;
        }
        /** @inheritdoc */
        render(leg, context, streamStack, partsToRender, ...args) {
            this.isAtLegStart = true;
            this.needStrokeLineAtLegEnd = false;
            super.render(leg, context, streamStack, partsToRender, ...args);
            if (this.needStrokeLineAtLegEnd) {
                this.strokeLine(context, this.styleBuffer[(this.activeStyleIndex + 1) % 2]);
                this.needStrokeLineAtLegEnd = false;
            }
        }
        /** @inheritdoc */
        renderVector(vector, isIngress, isEgress, leg, context, streamStack, ...args) {
            const style = this.styleSelector(vector, isIngress, isEgress, leg, streamStack.getProjection(), this.styleBuffer[this.activeStyleIndex], ...args);
            const previousStyle = this.styleBuffer[(this.activeStyleIndex + 1) % 2];
            const didStyleChange = !this.isAtLegStart && !FlightPathLegLineRenderer.areStylesEqual(style, previousStyle);
            const continuePath = !this.isAtLegStart && style.isContinuous && !didStyleChange;
            if (didStyleChange) {
                this.strokeLine(context, previousStyle);
                this.needStrokeLineAtLegEnd = false;
            }
            const circle = FlightPathUtils.setGeoCircleFromVector(vector, FlightPathLegLineRenderer.geoCircleCache[1]);
            this.pathRenderer.render(circle, vector.startLat, vector.startLon, vector.endLat, vector.endLon, streamStack, continuePath);
            this.activeStyleIndex = (this.activeStyleIndex + 1) % 2;
            this.isAtLegStart = false;
            this.needStrokeLineAtLegEnd = true;
        }
        /**
         * Applies a stroke to a canvas context.
         * @param context A canvas 2D rendering context.
         * @param style The style of the line to stroke.
         */
        strokeLine(context, style) {
            var _a, _b;
            if (style.outlineWidth > 0) {
                const outlineWidth = style.strokeWidth + 2 * style.outlineWidth;
                context.lineWidth = outlineWidth;
                context.strokeStyle = style.outlineStyle;
                context.setLineDash((_a = style.outlineDash) !== null && _a !== void 0 ? _a : FlightPathLegLineRenderer.EMPTY_DASH);
                context.stroke();
            }
            if (style.strokeWidth > 0) {
                context.lineWidth = style.strokeWidth;
                context.strokeStyle = style.strokeStyle;
                context.setLineDash((_b = style.strokeDash) !== null && _b !== void 0 ? _b : FlightPathLegLineRenderer.EMPTY_DASH);
                context.stroke();
            }
        }
        /**
         * Checks if two line styles are equal. Styles are considered equal if and only if their stroke and outline widths
         * are zero, or their stroke and outline widths, styles, and dash arrays are the same.
         * @param style1 The first style.
         * @param style2 The second style.
         * @returns Whether the two line styles are equal.
         */
        static areStylesEqual(style1, style2) {
            return (((style1.strokeWidth === 0 && style2.strokeWidth === 0)
                || (style1.strokeWidth === style2.strokeWidth
                    && style1.strokeStyle === style2.strokeStyle
                    && style1.strokeDash === style2.strokeDash)) && ((style1.outlineWidth === 0 && style2.outlineWidth === 0)
                || (style1.outlineWidth === style2.outlineWidth
                    && style1.outlineStyle === style2.outlineStyle
                    && style1.outlineDash === style2.outlineDash)));
        }
    }
    FlightPathLegLineRenderer.EMPTY_DASH = [];
    FlightPathLegLineRenderer.geoCircleCache = [new GeoCircle(new Float64Array(3), 0), new GeoCircle(new Float64Array(3), 0)];

    /**
     * A path stream which does nothing on any input.
     */
    class NullPathStream {
        /**
         * Does nothing.
         */
        beginPath() {
            // noop
        }
        /**
         * Does nothing.
         */
        moveTo() {
            // noop
        }
        /**
         * Does nothing.
         */
        lineTo() {
            // noop
        }
        /**
         * Does nothing.
         */
        bezierCurveTo() {
            // noop
        }
        /**
         * Does nothing.
         */
        quadraticCurveTo() {
            // noop
        }
        /**
         * Does nothing.
         */
        arc() {
            // noop
        }
        /**
         * Does nothing.
         */
        closePath() {
            // noop
        }
    }
    /** An instance of a {@link NullPathStream}. */
    NullPathStream.INSTANCE = new NullPathStream();
    /**
     * An abstract implementation of a path stream which sends a transformed version of its input to be consumed by another
     * stream.
     */
    class AbstractTransformingPathStream {
        /**
         * Constructor.
         * @param consumer The path stream that consumes this stream's transformed output.
         */
        constructor(consumer) {
            this.consumer = consumer;
        }
        /** @inheritdoc */
        getConsumer() {
            return this.consumer;
        }
        /** @inheritdoc */
        setConsumer(consumer) {
            this.consumer = consumer;
        }
    }
    /**
     * A path stream which sends its inputs unchanged to be consumed by another stream.
     */
    class PassThroughPathStream extends AbstractTransformingPathStream {
        /** @inheritdoc */
        beginPath() {
            this.consumer.beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            this.consumer.moveTo(x, y);
        }
        /** @inheritdoc */
        lineTo(x, y) {
            this.consumer.lineTo(x, y);
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            this.consumer.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            this.consumer.quadraticCurveTo(cpx, cpy, x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            this.consumer.arc(x, y, radius, startAngle, endAngle, counterClockwise);
        }
        /** @inheritdoc */
        closePath() {
            this.consumer.closePath();
        }
    }

    /**
     * Bitflags describing the relative location of a point with respect to a rectangular bounding box.
     */
    var Outcode;
    (function (Outcode) {
        Outcode[Outcode["Inside"] = 0] = "Inside";
        Outcode[Outcode["Left"] = 1] = "Left";
        Outcode[Outcode["Top"] = 2] = "Top";
        Outcode[Outcode["Right"] = 4] = "Right";
        Outcode[Outcode["Bottom"] = 8] = "Bottom";
    })(Outcode || (Outcode = {}));
    /**
     * A path stream which performs clipping to an axis-aligned rectangular bounding box before sending the clipped path
     * to another stream. Clipping is only supported for path segments added via the `lineTo()` and `arc()` methods. Path
     * segments added via `bezierCurveTo()` and `quadraticCurveTo()` will be passed to the consumer stream unclipped.
     */
    class ClippedPathStream extends AbstractTransformingPathStream {
        /**
         * Constructor.
         * @param consumer The path stream that consumes this stream's transformed output.
         * @param bounds A subscribable which provides the clipping bounds for this stream, as `[left, top, right, bottom]`.
         * Whenever the clipping bounds change, the state of this stream will be reset, as if `beginPath()` were called.
         */
        constructor(consumer, bounds) {
            super(consumer);
            this.bounds = bounds;
            this.boundsLines = [
                new Float64Array(3),
                new Float64Array(3),
                new Float64Array(3),
                new Float64Array(3)
            ];
            this.isBoundingRectNonZero = false;
            this.firstPoint = new Float64Array([NaN, NaN]);
            this.prevPoint = new Float64Array([NaN, NaN]);
            this.prevPointOutcode = 0;
            this.boundsSub = bounds.sub(this.onBoundsChanged.bind(this), true);
        }
        /** @inheritdoc */
        beginPath() {
            this.reset();
            this.consumer.beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            if (!this.isBoundingRectNonZero) {
                return;
            }
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (this.prevPoint[0] === x && this.prevPoint[1] === y) {
                return;
            }
            if (isNaN(this.firstPoint[0])) {
                Vec2Math.set(x, y, this.firstPoint);
            }
            Vec2Math.set(x, y, this.prevPoint);
            this.prevPointOutcode = this.getOutcode(x, y);
            if (this.prevPointOutcode === 0) {
                this.consumer.moveTo(x, y);
            }
        }
        /** @inheritdoc */
        lineTo(x, y) {
            if (!this.isBoundingRectNonZero) {
                return;
            }
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (this.prevPoint[0] === x && this.prevPoint[1] === y) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            const outcode = this.getOutcode(x, y);
            if ((this.prevPointOutcode | outcode) === 0) {
                // Both the previous point and current point are within bounds.
                this.consumer.lineTo(x, y);
            }
            else if ((this.prevPointOutcode & outcode) === 0) {
                // One or both of the previous point and current point are out of bounds, and the line connecting them may
                // cross through the bounding rect
                const bounds = this.bounds.get();
                const line = ClippedPathStream.getLineCoordinates(this.prevPoint[0], this.prevPoint[1], x, y, ClippedPathStream.vec3Cache[1]);
                let entryPoint, exitPoint;
                const outcodeOr = this.prevPointOutcode | outcode;
                if ((outcodeOr & ~(Outcode.Left | Outcode.Right)) === 0 || (outcodeOr & ~(Outcode.Top | Outcode.Bottom)) === 0) {
                    // The connecting line does not cross zones diagonally -> no need to check if the intersection of the line and
                    // boundary falls outside the bounds of the orthogonal axis.
                    // find entry point
                    for (let i = 0; i < 4; i++) {
                        if (this.prevPointOutcode & (1 << i)) {
                            entryPoint = ClippedPathStream.findLineLineIntersection(line, this.boundsLines[i], ClippedPathStream.vec2Cache[0]);
                            break;
                        }
                    }
                    // find exit point
                    for (let i = 0; i < 4; i++) {
                        if (outcode & (1 << i)) {
                            exitPoint = ClippedPathStream.findLineLineIntersection(line, this.boundsLines[i], ClippedPathStream.vec2Cache[1]);
                            break;
                        }
                    }
                }
                else {
                    // The connecting line crosses zones diagonally -> we need to check if the intersection of the line and each
                    // boundary falls outside the bounds of the orthogonal axis.
                    // find entry point
                    for (let i = 0; i < 4; i++) {
                        if (this.prevPointOutcode & (1 << i)) {
                            const boundsAxisIndex = (i + 1) % 2;
                            const intersection = ClippedPathStream.findLineLineIntersection(line, this.boundsLines[i], ClippedPathStream.vec2Cache[0]);
                            if (intersection && intersection[boundsAxisIndex] >= bounds[boundsAxisIndex] && intersection[boundsAxisIndex] <= bounds[boundsAxisIndex + 2]) {
                                entryPoint = intersection;
                                break;
                            }
                        }
                    }
                    // find exit point
                    for (let i = 0; i < 4; i++) {
                        if (outcode & (1 << i)) {
                            const boundsAxisIndex = (i + 1) % 2;
                            const intersection = ClippedPathStream.findLineLineIntersection(line, this.boundsLines[i], ClippedPathStream.vec2Cache[1]);
                            if (intersection && intersection[boundsAxisIndex] >= bounds[boundsAxisIndex] && intersection[boundsAxisIndex] <= bounds[boundsAxisIndex + 2]) {
                                exitPoint = intersection;
                                break;
                            }
                        }
                    }
                }
                if (entryPoint) {
                    this.consumer.moveTo(entryPoint[0], entryPoint[1]);
                }
                if (exitPoint) {
                    this.consumer.lineTo(exitPoint[0], exitPoint[1]);
                }
                else if (outcode === Outcode.Inside) {
                    this.consumer.lineTo(x, y);
                }
            }
            Vec2Math.set(x, y, this.prevPoint);
            this.prevPointOutcode = outcode;
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            if (!this.isBoundingRectNonZero) {
                return;
            }
            if (!(isFinite(x) && isFinite(y) && isFinite(cp1x) && isFinite(cp1y) && isFinite(cp2x) && isFinite(cp2y))) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            if (this.prevPointOutcode !== Outcode.Inside) {
                this.consumer.moveTo(this.prevPoint[0], this.prevPoint[1]);
            }
            this.consumer.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
            Vec2Math.set(x, y, this.prevPoint);
            this.prevPointOutcode = this.getOutcode(x, y);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            if (!this.isBoundingRectNonZero) {
                return;
            }
            if (!(isFinite(x) && isFinite(y) && isFinite(cpx) && isFinite(cpy))) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            if (this.prevPointOutcode !== Outcode.Inside) {
                this.consumer.moveTo(this.prevPoint[0], this.prevPoint[1]);
            }
            this.consumer.quadraticCurveTo(cpx, cpy, x, y);
            Vec2Math.set(x, y, this.prevPoint);
            this.prevPointOutcode = this.getOutcode(x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise = false) {
            if (!this.isBoundingRectNonZero) {
                return;
            }
            if (!(isFinite(x) && isFinite(y) && isFinite(radius) && isFinite(startAngle) && isFinite(endAngle))) {
                return;
            }
            if (radius === 0 || startAngle === endAngle) {
                return;
            }
            const pi2 = 2 * Math.PI;
            const directionSign = counterClockwise ? -1 : 1;
            if (Math.sign(endAngle - startAngle) !== directionSign) {
                // Replicate behavior of canvas context arc() when the sign of the difference between start and end angles
                // doesn't match the counterClockwise flag.
                const angleDiff = ((counterClockwise ? startAngle - endAngle : endAngle - startAngle) % pi2 + pi2) % pi2;
                endAngle = startAngle + angleDiff * directionSign;
            }
            // Canvas context arc() clamps angular width to 2pi, so we will too.
            const angularWidth = Math.min(pi2, (endAngle - startAngle) * directionSign);
            endAngle = startAngle + angularWidth * directionSign;
            const bounds = this.bounds.get();
            const radiusSq = radius * radius;
            const startPoint = Vec2Math.add(Vec2Math.set(x, y, ClippedPathStream.vec2Cache[2]), Vec2Math.setFromPolar(radius, startAngle, ClippedPathStream.vec2Cache[0]), ClippedPathStream.vec2Cache[2]);
            const startPointOutcode = this.getOutcode(startPoint[0], startPoint[1]);
            const endPoint = Vec2Math.add(Vec2Math.set(x, y, ClippedPathStream.vec2Cache[3]), Vec2Math.setFromPolar(radius, endAngle, ClippedPathStream.vec2Cache[0]), ClippedPathStream.vec2Cache[3]);
            const endPointOutcode = this.getOutcode(endPoint[0], endPoint[1]);
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(startPoint[0], startPoint[1]);
            }
            else if (!Vec2Math.equals(this.prevPoint, startPoint)) {
                this.lineTo(startPoint[0], startPoint[1]);
            }
            // Find all intersections of the arc circle with the clipping bounds; there can be up to 8 (two for each boundary
            // line).
            const intersections = ClippedPathStream.intersectionCache;
            let intersectionCount = 0;
            for (let i = 0; i < 4; i++) {
                const axisCoordIndex = i % 2;
                const crossAxisCoordIndex = (i + 1) % 2;
                const centerAxisCoord = i % 2 === 0 ? x : y;
                const centerCrossAxisCoord = i % 2 === 0 ? y : x;
                const deltaToBound = bounds[i] - centerAxisCoord;
                if (Math.abs(deltaToBound) < radius) {
                    const radialOffsetSign = axisCoordIndex === 0 ? 1 : -1;
                    const crossAxisBoundMin = bounds[crossAxisCoordIndex];
                    const crossAxisBoundMax = bounds[crossAxisCoordIndex + 2];
                    //const radialOffset = Math.acos(deltaToBound / radius);
                    const crossAxisOffset = Math.sqrt(radiusSq - deltaToBound * deltaToBound);
                    let intersectionRadialOffset;
                    {
                        const intersectionCrossAxisCoord = centerCrossAxisCoord + crossAxisOffset;
                        if (intersectionCrossAxisCoord >= crossAxisBoundMin && intersectionCrossAxisCoord <= crossAxisBoundMax) {
                            const intersection = intersections[intersectionCount];
                            intersection.point[axisCoordIndex] = bounds[i];
                            intersection.point[crossAxisCoordIndex] = intersectionCrossAxisCoord;
                            const radial = axisCoordIndex * Math.PI / 2 + (intersectionRadialOffset !== null && intersectionRadialOffset !== void 0 ? intersectionRadialOffset : (intersectionRadialOffset = Math.acos(MathUtils.clamp(deltaToBound / radius, -1, 1)))) * radialOffsetSign;
                            intersection.radial = (radial + pi2) % pi2; // [0, 2 * pi)
                            intersectionCount++;
                        }
                    }
                    {
                        const intersectionCrossAxisCoord = centerCrossAxisCoord - crossAxisOffset;
                        if (intersectionCrossAxisCoord >= crossAxisBoundMin && intersectionCrossAxisCoord <= crossAxisBoundMax) {
                            const intersection = intersections[intersectionCount];
                            intersection.point[axisCoordIndex] = bounds[i];
                            intersection.point[crossAxisCoordIndex] = intersectionCrossAxisCoord;
                            const radial = axisCoordIndex * Math.PI / 2 - (intersectionRadialOffset !== null && intersectionRadialOffset !== void 0 ? intersectionRadialOffset : (intersectionRadialOffset = Math.acos(MathUtils.clamp(deltaToBound / radius, -1, 1)))) * radialOffsetSign;
                            intersection.radial = (radial + pi2) % pi2; // [0, 2 * pi)
                            intersectionCount++;
                        }
                    }
                }
            }
            if (intersectionCount > 1) {
                // Set all unused intersection radials to infinity so they are guaranteed to be sorted last.
                for (let i = intersectionCount; i < intersections.length; i++) {
                    intersections[i].radial = Infinity;
                }
                // Sort the intersections such that they are in clockwise order.
                intersections.sort(ClippedPathStream.compareCircleBoundsIntersections);
            }
            // Begin at the start radial, then in order (either clockwise or counterclockwise depending on the arc direction)
            // iterate through the intersection points. At each intersection, move to the point if we are currently out of
            // bounds or path an arc from the last visited radial to the point if we are inbounds. Every time we visit an
            // intersection we go from out of bounds to in bounds and vice versa. Stop when the radial to the intersection
            // is past the end radial of the arc.
            let isOutside = startPointOutcode !== Outcode.Inside;
            let prevRadial = startAngle;
            let intersectionStartIndex = -1;
            let minAngularDiff = Infinity;
            for (let i = 0; i < intersectionCount; i++) {
                const angularDiff = MathUtils.diffAngle(startAngle * directionSign, intersections[i].radial * directionSign);
                if (angularDiff < minAngularDiff) {
                    intersectionStartIndex = i;
                    minAngularDiff = angularDiff;
                }
            }
            if (intersectionStartIndex >= 0) {
                let angularWidthRemaining = angularWidth;
                for (let i = 0; i < intersectionCount; i++) {
                    const index = (intersectionStartIndex + intersectionCount + i * directionSign) % intersectionCount;
                    const intersection = intersections[index];
                    const segmentAngularWidth = MathUtils.diffAngle(prevRadial * directionSign, intersection.radial * directionSign);
                    if (segmentAngularWidth >= angularWidthRemaining) {
                        angularWidthRemaining = 0;
                        break;
                    }
                    const currentRadial = prevRadial + segmentAngularWidth * directionSign;
                    if (isOutside) {
                        this.consumer.moveTo(intersection.point[0], intersection.point[1]);
                    }
                    else {
                        this.consumer.arc(x, y, radius, prevRadial, currentRadial, counterClockwise);
                    }
                    isOutside = !isOutside;
                    prevRadial = currentRadial;
                    angularWidthRemaining = (endAngle - prevRadial) * directionSign;
                }
            }
            if (!isOutside) {
                // If the last segment is not outside, then we will path an arc to the end radial.
                this.consumer.arc(x, y, radius, prevRadial, endAngle, counterClockwise);
            }
            else if (endPointOutcode === Outcode.Inside) {
                // If the last segment is outside but the endpoint is inside, then this means the endpoint is very close to the
                // clipping bounds and floating point error caused the discrepancy. In this case, we will not bother to draw an
                // arc because any such arc would be extremely short. Instead, we will move to the end point to ensure we leave
                // the consumer stream in the correct state for the next path command.
                this.consumer.moveTo(endPoint[0], endPoint[1]);
            }
            Vec2Math.copy(endPoint, this.prevPoint);
            this.prevPointOutcode = endPointOutcode;
        }
        /** @inheritdoc */
        closePath() {
            if (!isNaN(this.firstPoint[0])) {
                this.lineTo(this.firstPoint[0], this.firstPoint[1]);
            }
        }
        /**
         * Resets the state of this stream.
         */
        reset() {
            Vec2Math.set(NaN, NaN, this.firstPoint);
            Vec2Math.set(NaN, NaN, this.prevPoint);
            this.prevPointOutcode = 0;
        }
        /**
         * Gets the Cohen-Sutherland outcode for a point.
         * @param x The x-coordinate of the query point.
         * @param y The y-coordinate of the query point.
         * @returns The outcode for the point.
         */
        getOutcode(x, y) {
            const bounds = this.bounds.get();
            let code = 0;
            if (x < bounds[0]) {
                code |= Outcode.Left;
            }
            else if (x > bounds[2]) {
                code |= Outcode.Right;
            }
            if (y < bounds[1]) {
                code |= Outcode.Top;
            }
            else if (y > bounds[3]) {
                code |= Outcode.Bottom;
            }
            return code;
        }
        /**
         * Handles clipping bounds change events.
         */
        onBoundsChanged() {
            const bounds = this.bounds.get();
            Vec3Math.set(1, 0, -bounds[0], this.boundsLines[0]);
            Vec3Math.set(0, 1, -bounds[1], this.boundsLines[1]);
            Vec3Math.set(1, 0, -bounds[2], this.boundsLines[2]);
            Vec3Math.set(0, 1, -bounds[3], this.boundsLines[3]);
            this.isBoundingRectNonZero = bounds[0] < bounds[2] && bounds[1] < bounds[3];
            this.beginPath();
        }
        /**
         * Destroys this stream.
         */
        destroy() {
            this.boundsSub.destroy();
        }
        /**
         * Gets the line coordinate vector for a line passing through two points.
         * @param x1 The x-coordinate of the first point on the line.
         * @param y1 The y-coordinate of the first point on the line.
         * @param x2 The x-coordinate of the second point on the line.
         * @param y2 The y-coordinate of the second point on the line.
         * @param out A Float64Array object to which to write the result.
         * @returns The line coordinate vector of the line passing through the two points.
         */
        static getLineCoordinates(x1, y1, x2, y2, out) {
            const a = y1 - y2;
            const b = x2 - x1;
            const c = -(a * x1 + b * y1);
            return Vec3Math.set(a, b, c, out);
        }
        /**
         * Finds the intersection point between two lines in 2D Euclidean space.
         * @param line1 The line coordinate vector of the first line.
         * @param line2 The line coordinate vector of the second line.
         * @param out A Float64Array object to which to write the result.
         * @returns The intersection point of the two lines, or undefined if the two lines are parallel.
         */
        static findLineLineIntersection(line1, line2, out) {
            const cross = Vec3Math.cross(line1, line2, ClippedPathStream.vec3Cache[0]);
            const w = cross[2];
            if (w === 0) {
                return undefined;
            }
            return Vec2Math.set(cross[0] / w, cross[1] / w, out);
        }
        /**
         * Compares two circle-bounding box intersections and returns whether the first intersection's radial is less than,
         * greater than, or equal to the second's radial.
         * @param a The first intersection to compare.
         * @param b The second intersection to compare.
         * @returns A negative number if the first intersection's radial is less than the second, a positive number if the
         * first intersection's radial is greater than the second, or zero if both intersections' radials are equal.
         */
        static compareCircleBoundsIntersections(a, b) {
            return a.radial - b.radial;
        }
    }
    ClippedPathStream.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2), new Float64Array(2)];
    ClippedPathStream.vec3Cache = [new Float64Array(3), new Float64Array(3)];
    ClippedPathStream.intersectionCache = Array.from({ length: 8 }, () => {
        return { point: new Float64Array(2), radial: Infinity };
    });

    /**
     * A path stream which transforms a path stream in geographic spherical coordinates to one in projected planar
     * coordinates.
     */
    class GeoProjectionPathStream extends AbstractTransformingPathStream {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(consumer, projection, arg1, arg2, arg3) {
            super(consumer);
            this.projection = projection;
            this.firstPoint = new GeoPoint(NaN, NaN);
            this.prevPoint = new GeoPoint(NaN, NaN);
            this.prevPointProjected = new Float64Array(2);
            this.resampleHandler = this.onResampled.bind(this);
            if (arg1 instanceof GeoCircleResampler) {
                this.resampler = arg1;
            }
            else {
                this.resampler = new GeoCircleResampler(arg1, arg2, arg3);
            }
        }
        /**
         * Gets the projection used by this stream.
         * @returns The projection used by this stream.
         */
        getProjection() {
            return this.projection;
        }
        /**
         * Sets the projection used by this stream.
         * @param projection A projection.
         */
        setProjection(projection) {
            this.projection = projection;
        }
        /** @inheritdoc */
        beginPath() {
            this.reset();
            this.consumer.beginPath();
        }
        /**
         * Moves to a specified point.
         * @param lon The longitude of the point to which to move, in degrees.
         * @param lat The latitude of the point to which to move, in degrees.
         */
        moveTo(lon, lat) {
            if (!(isFinite(lon) && isFinite(lat))) {
                return;
            }
            if (isNaN(this.firstPoint.lat)) {
                this.firstPoint.set(lat, lon);
            }
            this.prevPoint.set(lat, lon);
            const projected = this.projection.project(this.prevPoint, this.prevPointProjected);
            this.consumer.moveTo(projected[0], projected[1]);
        }
        /**
         * Paths a great-circle arc from the current point to a specified point.
         * @param lon The longitude of the end point, in degrees.
         * @param lat The latitude of the end point, in degrees.
         * @throws Error if the specified point is antipodal to the last pathed point.
         */
        lineTo(lon, lat) {
            if (!(isFinite(lon) && isFinite(lat))) {
                return;
            }
            if (!isNaN(this.prevPoint.lat) && this.prevPoint.equals(lat, lon)) {
                return;
            }
            if (isNaN(this.prevPoint.lat)) {
                this.moveTo(lon, lat);
                return;
            }
            const point = GeoProjectionPathStream.geoPointCache[0].set(lat, lon);
            const circle = GeoProjectionPathStream.geoCircleCache[0].setAsGreatCircle(this.prevPoint, point);
            if (!isFinite(circle.center[0])) {
                throw new Error(`Cannot unambiguously path a great circle from ${this.prevPoint.lat} lat, ${this.prevPoint.lon} lon to ${lat} lat, ${lon} lon`);
            }
            this.resampler.resample(this.projection, circle, this.prevPoint, point, this.resampleHandler);
            this.prevPoint.set(lat, lon);
        }
        /**
         * Not supported by this path stream.
         * @throws Error when called.
         */
        bezierCurveTo() {
            throw new Error('GeodesicResamplerStream: bezierCurveTo() is not supported');
        }
        /**
         * Not supported by this path stream.
         * @throws Error when called.
         */
        quadraticCurveTo() {
            throw new Error('GeodesicResamplerStream: quadraticCurveTo() is not supported');
        }
        /**
         * Paths a small-circle arc.
         * @param lon The longitude of the center of the circle containing the arc, in degrees.
         * @param lat The latitude of the center of the circle containing the arc, in degrees.
         * @param radius The radius of the arc, in great-arc radians.
         * @param startAngle If the center of the circle containing the arc is not one of the poles, the true bearing, in
         * degrees, from the center of the circle to the start of the arc; otherwise the longitude, in degrees, of the start
         * of the arc.
         * @param endAngle If the center of the circle containing the arc is not one of the poles, the true bearing, in
         * degrees, from the center of the circle to the end of the arc; otherwise the longitude, in degrees, of the end of
         * the arc.
         * @param counterClockwise Whether the arc should be drawn counterclockwise. False by default.
         */
        arc(lon, lat, radius, startAngle, endAngle, counterClockwise) {
            if (!(isFinite(lon) && isFinite(lat) && isFinite(radius) && isFinite(startAngle) && isFinite(endAngle))) {
                return;
            }
            if (radius === 0 || Math.abs(startAngle - endAngle) <= GeoCircle.ANGULAR_TOLERANCE * Avionics.Utils.RAD2DEG) {
                return;
            }
            if (MathUtils.diffAngle(startAngle * Avionics.Utils.DEG2RAD, endAngle * Avionics.Utils.DEG2RAD, false) <= GeoCircle.ANGULAR_TOLERANCE) {
                // Since we early return above if startAngle and endAngle are equal, hitting this case means they are a multiple
                // of 360 degrees apart. The resampler will interpret them as being the same point and won't draw a full circle
                // so we will split the arc into two.
                const midAngle = startAngle + 180 * Math.sign(endAngle - startAngle);
                this.arc(lon, lat, radius, startAngle, midAngle, counterClockwise);
                this.arc(lon, lat, radius, midAngle, endAngle, counterClockwise);
                return;
            }
            const center = GeoProjectionPathStream.geoPointCache[1].set(lat, lon);
            const start = GeoProjectionPathStream.geoPointCache[2];
            const end = GeoProjectionPathStream.geoPointCache[3];
            if (Math.abs(lat) >= 90 - GeoCircle.ANGULAR_TOLERANCE * Avionics.Utils.RAD2DEG) {
                // The center of the arc circle is one of the poles
                const circleLat = Math.sign(lat) * (MathUtils.HALF_PI - radius) * Avionics.Utils.RAD2DEG;
                start.set(circleLat, startAngle);
                end.set(circleLat, endAngle);
            }
            else {
                center.offset(startAngle, radius, start);
                center.offset(endAngle, radius, end);
            }
            if (isNaN(start.lat) || isNaN(start.lon) || isNaN(end.lat) || isNaN(end.lon)) {
                return;
            }
            if (isNaN(this.prevPoint.lat)) {
                this.moveTo(start.lon, start.lat);
            }
            else if (!start.equals(this.prevPoint)) {
                this.lineTo(start.lon, start.lat);
            }
            const circle = GeoProjectionPathStream.geoCircleCache[0].set(center, radius);
            if (!counterClockwise) {
                circle.reverse();
            }
            this.resampler.resample(this.projection, circle, start, end, this.resampleHandler);
            this.prevPoint.set(end);
        }
        /**
         * Paths a great-circle arc from the current point to the first point defined by the current path.
         */
        closePath() {
            if (!isNaN(this.firstPoint.lat)) {
                this.lineTo(this.firstPoint.lon, this.firstPoint.lat);
            }
        }
        /**
         * Resets the state of this stream.
         */
        reset() {
            this.firstPoint.set(NaN, NaN);
            this.prevPoint.set(NaN, NaN);
        }
        /**
         * Handles resampled points.
         * @param vector A vector which describes the projected path terminating at the resampled point.
         */
        onResampled(vector) {
            switch (vector.type) {
                case 'start':
                    return;
                case 'line':
                    this.consumer.lineTo(vector.projected[0], vector.projected[1]);
                    break;
                case 'arc':
                    this.consumer.arc(vector.projectedArcCenter[0], vector.projectedArcCenter[1], vector.projectedArcRadius, vector.projectedArcStartAngle, vector.projectedArcEndAngle, vector.projectedArcStartAngle > vector.projectedArcEndAngle);
                    break;
            }
            Vec2Math.copy(vector.projected, this.prevPointProjected);
        }
    }
    GeoProjectionPathStream.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0), new GeoPoint(0, 0), new GeoPoint(0, 0)];
    GeoProjectionPathStream.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /**
     * A {@link TransformingPathStream} which applies an affine transformation to its input.
     *
     * The types of transformation supported by this class are:
     * * Translation.
     * * Uniform scaling.
     * * Rotation.
     */
    class AffineTransformPathStream extends AbstractTransformingPathStream {
        constructor() {
            super(...arguments);
            this.transform = new Transform2D();
            this.concatCache = [];
            this.scale = 1;
            this.rotation = 0;
        }
        /**
         * Adds a translation to this stream's transformation.
         * @param x The x translation.
         * @param y The y translation.
         * @param order The order in which to add the translation (defaults to `'after'`):
         * * `'before'` - Applies the translation before this stream's current transformation.
         * * `'after'` - Applies the translation after this stream's current transformation.
         * @returns This stream, after its transformation has been changed.
         */
        addTranslation(x, y, order = 'after') {
            const translation = AffineTransformPathStream.transformCache[0].toTranslation(x, y);
            if (order === 'before') {
                this.concatCache[0] = translation;
                this.concatCache[1] = this.transform;
            }
            else {
                this.concatCache[0] = this.transform;
                this.concatCache[1] = translation;
            }
            Transform2D.concat(this.transform, this.concatCache);
            return this;
        }
        /**
         * Adds a uniform scaling to this stream's transformation.
         * @param factor The scaling factor.
         * @param order The order in which to add the translation (defaults to `'after'`):
         * * `'before'` - Applies the scaling before this stream's current transformation.
         * * `'after'` - Applies the scaling after this stream's current transformation.
         * @returns This stream, after its transformation has been changed.
         */
        addScale(factor, order = 'after') {
            const scale = AffineTransformPathStream.transformCache[0].toScale(factor, factor);
            if (order === 'before') {
                this.concatCache[0] = scale;
                this.concatCache[1] = this.transform;
            }
            else {
                this.concatCache[0] = this.transform;
                this.concatCache[1] = scale;
            }
            Transform2D.concat(this.transform, this.concatCache);
            this.updateScaleRotation();
            return this;
        }
        /**
         * Adds a rotation to this stream's transformation.
         * @param angle The rotation angle, in radians.
         * @param order The order in which to add the translation (defaults to `'after'`):
         * * `'before'` - Applies the rotation before this stream's current transformation.
         * * `'after'` - Applies the rotation after this stream's current transformation.
         * @returns This stream, after its transformation has been changed.
         */
        addRotation(angle, order = 'after') {
            const rotation = AffineTransformPathStream.transformCache[0].toRotation(angle);
            if (order === 'before') {
                this.concatCache[0] = rotation;
                this.concatCache[1] = this.transform;
            }
            else {
                this.concatCache[0] = this.transform;
                this.concatCache[1] = rotation;
            }
            Transform2D.concat(this.transform, this.concatCache);
            this.updateScaleRotation();
            return this;
        }
        /**
         * Resets this stream's transformation to the identity transformation.
         * @returns This stream, after its transformation has been changed.
         */
        resetTransform() {
            this.transform.toIdentity();
            this.updateScaleRotation();
            return this;
        }
        /** @inheritdoc */
        beginPath() {
            this.consumer.beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            const transformed = this.applyTransform(x, y);
            this.consumer.moveTo(transformed[0], transformed[1]);
        }
        /** @inheritdoc */
        lineTo(x, y) {
            const transformed = this.applyTransform(x, y);
            this.consumer.lineTo(transformed[0], transformed[1]);
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            const cp1Transformed = this.applyTransform(cp1x, cp1y);
            cp1x = cp1Transformed[0];
            cp1y = cp1Transformed[1];
            const cp2Transformed = this.applyTransform(cp2x, cp2y);
            cp2x = cp2Transformed[0];
            cp2y = cp2Transformed[1];
            const endTransformed = this.applyTransform(x, y);
            x = endTransformed[0];
            y = endTransformed[1];
            this.consumer.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            const cpTransformed = this.applyTransform(cpx, cpy);
            cpx = cpTransformed[0];
            cpy = cpTransformed[1];
            const endTransformed = this.applyTransform(x, y);
            x = endTransformed[0];
            y = endTransformed[1];
            this.consumer.quadraticCurveTo(cpx, cpy, x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            const transformed = this.applyTransform(x, y);
            this.consumer.arc(transformed[0], transformed[1], radius * this.scale, startAngle + this.rotation, endAngle + this.rotation, counterClockwise);
        }
        /** @inheritdoc */
        closePath() {
            this.consumer.closePath();
        }
        /**
         * Updates this stream's cached scale and rotation values from its transformation.
         */
        updateScaleRotation() {
            const params = this.transform.getParameters();
            this.scale = Math.sqrt(params[0] * params[0] + params[3] * params[3]);
            this.rotation = Math.atan2(params[3], params[0]);
        }
        /**
         * Applies this stream's transformation to a point.
         * @param x The x-coordinate of the point to transform.
         * @param y The y-coordinate of the point to transform.
         * @returns The transformed point.
         */
        applyTransform(x, y) {
            const vec = Vec2Math.set(x, y, AffineTransformPathStream.vec2Cache[0]);
            return this.transform.apply(vec, vec);
        }
    }
    AffineTransformPathStream.vec2Cache = [new Float64Array(2)];
    AffineTransformPathStream.transformCache = [new Transform2D()];

    /**
     * A {@link TransformingPathStream} which converts an input path into path commands to draw a repeating pattern along
     * the input path.
     */
    class PatternPathStream {
        /**
         * Constructor.
         * @param consumer The path stream that consumes this stream's transformed output.
         * @param pattern The pattern drawn by this stream. If the pattern is `null`, then this stream will pass through path
         * commands to its consumer without transforming them into a pattern.
         */
        constructor(consumer, pattern) {
            this.pattern = pattern;
            this.clipBounds = VecNSubject.createFromVector(new Float64Array([Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]));
            this.firstPoint = new Float64Array([NaN, NaN]);
            this.prevPoint = new Float64Array([NaN, NaN]);
            this.distanceLeft = NaN;
            this.transformStream = new AffineTransformPathStream(consumer);
            this.clipStream = new ClippedPathStream(this.transformStream, this.clipBounds);
        }
        /**
         * Gets the pattern drawn by this stream.
         * @returns The pattern drawn by this stream.
         */
        getPattern() {
            return this.pattern;
        }
        /**
         * Sets the pattern drawn by this stream. If the pattern is `null`, then this stream will pass through path commands
         * to its consumer without transforming them into a pattern.
         * @param pattern A pattern.
         */
        setPattern(pattern) {
            if (!isNaN(this.distanceLeft) && this.pattern) {
                this.distanceLeft = Math.max(0, this.distanceLeft - this.pattern.anchor * this.pattern.length);
            }
            this.pattern = pattern;
            if (pattern) {
                if (!isNaN(this.distanceLeft)) {
                    this.distanceLeft += pattern.anchor * pattern.length;
                }
            }
            else {
                this.distanceLeft = NaN;
            }
        }
        /** @inheritdoc */
        getConsumer() {
            return this.transformStream.getConsumer();
        }
        /** @inheritdoc */
        setConsumer(consumer) {
            this.transformStream.setConsumer(consumer);
        }
        /** @inheritdoc */
        beginPath() {
            this.reset();
            this.getConsumer().beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (this.prevPoint[0] === x && this.prevPoint[1] === y) {
                return;
            }
            if (isNaN(this.firstPoint[0])) {
                Vec2Math.set(x, y, this.firstPoint);
            }
            Vec2Math.set(x, y, this.prevPoint);
            this.distanceLeft = NaN;
        }
        /** @inheritdoc */
        lineTo(x, y) {
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (this.prevPoint[0] === x && this.prevPoint[1] === y) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            if (this.pattern) {
                const x0 = this.prevPoint[0];
                const y0 = this.prevPoint[1];
                const dx = x - x0;
                const dy = y - y0;
                const distance = Math.hypot(dx, dy);
                const angle = Math.atan2(dy, dx);
                this.transformStream.resetTransform()
                    .addRotation(angle)
                    .addTranslation(x0, y0);
                let nextLength = this.pattern.length;
                if (nextLength > 0) {
                    let nextAnchor = Utils.Clamp(this.pattern.anchor, 0, 1);
                    let distanceToNextAnchor = isNaN(this.distanceLeft) ? nextLength * nextAnchor : this.distanceLeft;
                    this.distanceLeft = distance;
                    while (this.distanceLeft >= 0 && distanceToNextAnchor <= this.distanceLeft && this.distanceLeft >= nextLength * 0.5) {
                        this.distanceLeft -= distanceToNextAnchor;
                        this.transformStream.addTranslation(distanceToNextAnchor, 0, 'before');
                        this.clipBounds.set(-nextLength * nextAnchor, Number.MIN_SAFE_INTEGER, nextLength * (1 - nextAnchor), Number.MAX_SAFE_INTEGER);
                        this.clipStream.beginPath();
                        this.pattern.draw(this.clipStream);
                        const distanceToPatternEnd = nextLength * (1 - nextAnchor);
                        this.distanceLeft -= distanceToPatternEnd;
                        this.transformStream.addTranslation(distanceToPatternEnd, 0, 'before');
                        nextLength = this.pattern.length;
                        if (nextLength <= 0) {
                            this.distanceLeft = NaN;
                            break;
                        }
                        nextAnchor = Utils.Clamp(this.pattern.anchor, 0, 1);
                        distanceToNextAnchor = nextLength * nextAnchor;
                    }
                    if (!isNaN(this.distanceLeft)) {
                        this.distanceLeft = Math.max(0, distanceToNextAnchor - this.distanceLeft);
                    }
                }
            }
            else {
                this.getConsumer().lineTo(x, y);
            }
            Vec2Math.set(x, y, this.prevPoint);
        }
        /**
         * Not supported by this path stream. Calling this method will execute a `moveTo()` command to the specified end
         * point.
         * @param cp1x The x-coordinate of the first control point.
         * @param cp1y The y-coordinate of the first control point.
         * @param cp2x The x-coordinate of the second control point.
         * @param cp2y The y-coordinate of the second control point.
         * @param x The x-coordinate of the end point.
         * @param y The y-coordinate of the end point.
         */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            this.moveTo(x, y);
        }
        /**
         * Not supported by this path stream. Calling this method will execute a `moveTo()` command to the specified end
         * point.
         * @param cpx The x-coordinate of the control point.
         * @param cpy The y-coordinate of the control point.
         * @param x The x-coordinate of the end point.
         * @param y The y-coordinate of the end point.
         */
        quadraticCurveTo(cpx, cpy, x, y) {
            this.moveTo(x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            if (!(isFinite(x) && isFinite(y) && isFinite(radius) && isFinite(startAngle) && isFinite(endAngle))) {
                return;
            }
            if (radius === 0 || startAngle === endAngle) {
                return;
            }
            const startPoint = Vec2Math.add(Vec2Math.set(x, y, PatternPathStream.vec2Cache[0]), Vec2Math.setFromPolar(radius, startAngle, PatternPathStream.vec2Cache[1]), PatternPathStream.vec2Cache[0]);
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(startPoint[0], startPoint[1]);
            }
            else if (!Vec2Math.equals(this.prevPoint, startPoint)) {
                this.lineTo(startPoint[0], startPoint[1]);
            }
            if (this.pattern) {
                const pi2 = 2 * Math.PI;
                const directionSign = counterClockwise ? -1 : 1;
                if (Math.sign(endAngle - startAngle) !== directionSign) {
                    // Replicate behavior of canvas context arc() when the sign of the difference between start and end angles
                    // doesn't match the counterClockwise flag.
                    const angleDiff = ((counterClockwise ? startAngle - endAngle : endAngle - startAngle) % pi2 + pi2) % pi2;
                    endAngle = startAngle + angleDiff * directionSign;
                }
                // Clamp to 2pi because we don't need to draw anything past a full circle.
                const angularWidth = (endAngle - startAngle) * directionSign;
                const distance = angularWidth * radius;
                let nextLength = this.pattern.length;
                if (nextLength > 0) {
                    let nextAnchor = Utils.Clamp(this.pattern.anchor, 0, 1);
                    let distanceToNextAnchor = isNaN(this.distanceLeft) ? nextLength * nextAnchor : this.distanceLeft;
                    let angle = startAngle;
                    this.distanceLeft = distance;
                    while (this.distanceLeft >= 0 && distanceToNextAnchor <= this.distanceLeft && this.distanceLeft >= nextLength * 0.5) {
                        this.distanceLeft -= distanceToNextAnchor;
                        angle += distanceToNextAnchor / radius * directionSign;
                        this.transformStream.resetTransform()
                            .addRotation(Math.PI / 2 * directionSign)
                            .addTranslation(radius, 0)
                            .addRotation(angle)
                            .addTranslation(x, y);
                        this.clipBounds.set(-nextLength * nextAnchor, Number.MIN_SAFE_INTEGER, nextLength * (1 - nextAnchor), Number.MAX_SAFE_INTEGER);
                        this.clipStream.beginPath();
                        this.pattern.draw(this.clipStream);
                        const distanceToPatternEnd = nextLength * (1 - nextAnchor);
                        this.distanceLeft -= distanceToPatternEnd;
                        angle += distanceToPatternEnd / radius * directionSign;
                        nextLength = this.pattern.length;
                        if (nextLength <= 0) {
                            this.distanceLeft = NaN;
                            break;
                        }
                        nextAnchor = Utils.Clamp(this.pattern.anchor, 0, 1);
                        distanceToNextAnchor = nextLength * nextAnchor;
                    }
                    if (!isNaN(this.distanceLeft)) {
                        this.distanceLeft = Math.max(0, distanceToNextAnchor - this.distanceLeft);
                    }
                }
            }
            else {
                this.getConsumer().arc(x, y, radius, startAngle, endAngle, counterClockwise);
            }
            Vec2Math.add(Vec2Math.set(x, y, PatternPathStream.vec2Cache[0]), Vec2Math.setFromPolar(radius, endAngle, PatternPathStream.vec2Cache[1]), this.prevPoint);
        }
        /** @inheritdoc */
        closePath() {
            if (!isNaN(this.firstPoint[0])) {
                this.lineTo(this.firstPoint[0], this.firstPoint[1]);
            }
        }
        /**
         * Resets the state of this stream.
         */
        reset() {
            Vec2Math.set(NaN, NaN, this.firstPoint);
            Vec2Math.set(NaN, NaN, this.prevPoint);
            this.distanceLeft = NaN;
        }
    }
    PatternPathStream.vec2Cache = [new Float64Array(2), new Float64Array(2)];

    /**
     * A stack of {@link TransformingPathStream}s. Inputs are passed through the entire stack from top to bottom before the
     * final transformed output is sent to a consuming stream.
     */
    class TransformingPathStreamStack extends AbstractTransformingPathStream {
        constructor() {
            super(...arguments);
            this.stack = [];
        }
        /**
         * Adds a transforming path stream to the top of this stack.
         * @param stream A transforming path stream.
         */
        push(stream) {
            var _a;
            stream.setConsumer((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer);
            this.stack.push(stream);
        }
        /**
         * Removes the top-most path stream from this stack. The removed stream will have its consumer set to
         * {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        pop() {
            const removed = this.stack.pop();
            removed === null || removed === void 0 ? void 0 : removed.setConsumer(NullPathStream.INSTANCE);
            return removed;
        }
        /**
         * Adds a transforming path stream to the bottom of this stack.
         * @param stream A transforming path stream.
         */
        unshift(stream) {
            const displaced = this.stack[0];
            displaced === null || displaced === void 0 ? void 0 : displaced.setConsumer(stream);
            stream.setConsumer(this.consumer);
            this.stack.unshift(stream);
        }
        /**
         * Removes the bottom-most path stream from this stack. The removed stream will have its consumer set to
         * {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        shift() {
            var _a;
            const removed = this.stack.shift();
            removed === null || removed === void 0 ? void 0 : removed.setConsumer(NullPathStream.INSTANCE);
            (_a = this.stack[0]) === null || _a === void 0 ? void 0 : _a.setConsumer(this.consumer);
            return removed;
        }
        /** @inheritdoc */
        setConsumer(consumer) {
            var _a;
            (_a = this.stack[0]) === null || _a === void 0 ? void 0 : _a.setConsumer(consumer);
            super.setConsumer(consumer);
        }
        /** @inheritdoc */
        beginPath() {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).moveTo(x, y);
        }
        /** @inheritdoc */
        lineTo(x, y) {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).lineTo(x, y);
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).quadraticCurveTo(cpx, cpy, x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            var _a;
            ((_a = this.stack[this.stack.length - 1]) !== null && _a !== void 0 ? _a : this.consumer).arc(x, y, radius, startAngle, endAngle, counterClockwise);
        }
        /** @inheritdoc */
        closePath() {
            this.stack[this.stack.length - 1].closePath();
        }
    }

    /**
     * Renders arcs along geo circles as repeating patterns.
     */
    class GeoCirclePatternRenderer {
        constructor() {
            this.pathRenderer = new GeoCirclePathRenderer();
            this.patternStream = new PatternPathStream(NullPathStream.INSTANCE, null);
        }
        /**
         * Renders an arc along a geo circle to a canvas.
         * @param circle The geo circle containing the arc to render.
         * @param startLat The latitude of the start of the arc, in degrees.
         * @param startLon The longitude of the start of the arc, in degrees.
         * @param endLat The latitude of the end of the arc, in degrees.
         * @param endLon The longitude of the end of the arc, in degrees.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream stack to which to render.
         * @param pattern The pattern to render.
         * @param continuePath Whether to continue the previously rendered path. If true, a discontinuity in the rendered
         * path will not be inserted before the arc is rendered. This may lead to undesired artifacts if the previously
         * rendered path does not terminate at the point where the projected arc starts. Defaults to false.
         */
        render(circle, startLat, startLon, endLat, endLon, context, streamStack, pattern, continuePath = false) {
            this.patternStream.setPattern(pattern);
            streamStack.unshiftPostProjected(this.patternStream);
            this.pathRenderer.render(circle, startLat, startLon, endLat, endLon, streamStack, continuePath);
            streamStack.shiftPostProjected();
        }
    }

    /**
     * Renders flight path vectors as repeating patterns.
     */
    class FlightPathVectorPatternRenderer {
        constructor() {
            this.renderer = new GeoCirclePatternRenderer();
        }
        /**
         * Renders a flight path vector to a canvas.
         * @param vector The flight path vector to render.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream to which to render.
         * @param pattern The pattern to render.
         * @param continuePath Whether to continue the previously rendered path. If true, a discontinuity in the rendered
         * path will not be inserted before the vector is rendered. This may lead to undesired artifacts if the previously
         * rendered path does not terminate at the point where the projected vector starts. Defaults to false.
         */
        render(vector, context, streamStack, pattern, continuePath = false) {
            const circle = FlightPathUtils.setGeoCircleFromVector(vector, FlightPathVectorPatternRenderer.geoCircleCache[0]);
            this.renderer.render(circle, vector.startLat, vector.startLon, vector.endLat, vector.endLon, context, streamStack, pattern, continuePath);
        }
    }
    FlightPathVectorPatternRenderer.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /**
     * Renders flight plan leg paths as repeating patterns, with support for different patterns for each flight path vector
     * in the leg.
     */
    class FlightPathLegPatternRenderer extends AbstractFlightPathLegRenderer {
        /**
         * Constructor.
         * @param styleSelector A function which selects a style for each rendered vector.
         */
        constructor(styleSelector) {
            super();
            this.styleSelector = styleSelector;
            this.pathRenderer = new FlightPathVectorPatternRenderer();
            this.style = {
                pattern: null,
                isContinuous: false
            };
            this.isAtDiscontinuity = false;
        }
        /** @inheritdoc */
        render(leg, context, streamStack, partsToRender, ...args) {
            this.isAtDiscontinuity = true;
            super.render(leg, context, streamStack, partsToRender, ...args);
        }
        /** @inheritdoc */
        renderVector(vector, isIngress, isEgress, leg, context, streamStack, ...args) {
            const style = this.styleSelector(vector, isIngress, isEgress, leg, streamStack.getProjection(), this.style, ...args);
            const continuePath = !this.isAtDiscontinuity && style.isContinuous;
            if (style.pattern) {
                this.pathRenderer.render(vector, context, streamStack, style.pattern, continuePath);
                this.isAtDiscontinuity = false;
            }
            else {
                this.isAtDiscontinuity = true;
            }
        }
    }

    /**
     * Renders arcs along geo circles as curved lines.
     */
    class GeoCircleLineRenderer {
        constructor() {
            this.pathRenderer = new GeoCirclePathRenderer();
        }
        /**
         * Renders an arc along a geo circle to a canvas.
         * @param circle The geo circle containing the arc to render.
         * @param startLat The latitude of the start of the arc, in degrees.
         * @param startLon The longitude of the start of the arc, in degrees.
         * @param endLat The latitude of the end of the arc, in degrees.
         * @param endLon The longitude of the end of the arc, in degrees.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream stack to which to render.
         * @param width The width of the rendered line.
         * @param style The style of the rendered line.
         * @param dash The dash array of the rendered line. Defaults to no dash.
         * @param outlineWidth The width of the outline, in pixels. Defaults to 0 pixels.
         * @param outlineStyle The style of the outline. Defaults to `'black'`.
         * @param lineCap The line cap style to use. Defaults to `'butt'`.
         */
        render(circle, startLat, startLon, endLat, endLon, context, streamStack, width, style, dash, outlineWidth = 0, outlineStyle = 'black', lineCap = 'butt') {
            this.pathRenderer.render(circle, startLat, startLon, endLat, endLon, streamStack);
            if (outlineWidth > 0) {
                context.lineWidth = width + (outlineWidth * 2);
                context.strokeStyle = outlineStyle;
                context.lineCap = lineCap;
                context.setLineDash(dash !== null && dash !== void 0 ? dash : GeoCircleLineRenderer.EMPTY_DASH);
                context.stroke();
            }
            context.lineWidth = width;
            context.strokeStyle = style;
            context.lineCap = lineCap;
            context.setLineDash(dash !== null && dash !== void 0 ? dash : GeoCircleLineRenderer.EMPTY_DASH);
            context.stroke();
        }
    }
    GeoCircleLineRenderer.EMPTY_DASH = [];

    /**
     * Renders flight path vectors as a curved line.
     */
    class FlightPathVectorLineRenderer {
        constructor() {
            this.renderer = new GeoCircleLineRenderer();
        }
        /**
         * Renders a flight path vector to a canvas.
         * @param vector The flight path vector to render.
         * @param context The canvas 2D rendering context to which to render.
         * @param streamStack The path stream to which to render.
         * @param width The width of the rendered line.
         * @param style The style of the rendered line.
         * @param dash The dash array of the rendered line. Defaults to no dash.
         * @param outlineWidth The width of the outline, in pixels. Defaults to 0 pixels.
         * @param outlineStyle The style of the outline. Defaults to `'black'`.
         * @param lineCap The line cap style to use. Defaults to `'butt'`.
         */
        render(vector, context, streamStack, width, style, dash, outlineWidth, outlineStyle, lineCap = 'butt') {
            const circle = FlightPathUtils.setGeoCircleFromVector(vector, FlightPathVectorLineRenderer.geoCircleCache[0]);
            this.renderer.render(circle, vector.startLat, vector.startLon, vector.endLat, vector.endLon, context, streamStack, width, style, dash, outlineWidth, outlineStyle, lineCap);
        }
    }
    FlightPathVectorLineRenderer.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /**
     * A stack of {@link TransformingPathStream}s which transforms an input in spherical geographic coordinates to planar
     * projected coordinates. The stack contains two sub-stacks: a pre-projected stack which transforms the path before
     * it is projected, and a post-projected stack which transforms the projected path before it is sent to the consumer.
     * Transforming streams can be added to the top and bottom of each sub-stack. The input will be passed through each
     * stream in the pre-projected stack from top to bottom, then projected, then passed through each stream in the post-
     * projected stack from top to bottom, and the final transformed output will be passed to the consumer.
     */
    class GeoProjectionPathStreamStack extends AbstractTransformingPathStream {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(consumer, projection, arg1, arg2, arg3) {
            super(consumer);
            this.postStack = new TransformingPathStreamStack(consumer);
            if (arg1 instanceof GeoCircleResampler) {
                this.projectionStream = new GeoProjectionPathStream(this.postStack, projection, arg1);
            }
            else {
                this.projectionStream = new GeoProjectionPathStream(this.postStack, projection, arg1, arg2, arg3);
            }
            this.preStack = new TransformingPathStreamStack(this.projectionStream);
        }
        /**
         * Gets the projection used by this stream.
         * @returns The projection used by this stream.
         */
        getProjection() {
            return this.projectionStream.getProjection();
        }
        /**
         * Sets the projection used by this stream.
         * @param projection A projection.
         */
        setProjection(projection) {
            this.projectionStream.setProjection(projection);
        }
        /**
         * Adds a transforming path stream to the top of the pre-projected stack.
         * @param stream A transforming path stream.
         */
        pushPreProjected(stream) {
            this.preStack.push(stream);
        }
        /**
         * Removes the top-most path stream from the pre-projected stack. The removed stream will have its consumer set to
         * {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        popPreProjected() {
            return this.preStack.pop();
        }
        /**
         * Adds a transforming path stream to the bottom of the pre-projected stack.
         * @param stream A transforming path stream.
         */
        unshiftPreProjected(stream) {
            this.preStack.unshift(stream);
        }
        /**
         * Removes the bottom-most path stream from the pre-projected stack. The removed stream will have its consumer set to
         * {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        shiftPreProjected() {
            return this.preStack.shift();
        }
        /**
         * Adds a transforming path stream to the top of the post-projected stack.
         * @param stream A transforming path stream.
         */
        pushPostProjected(stream) {
            this.postStack.push(stream);
        }
        /**
         * Removes the top-most path stream from the post-projected stack. The removed stream will have its consumer set to
         * {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        popPostProjected() {
            return this.postStack.pop();
        }
        /**
         * Adds a transforming path stream to the bottom of the post-projected stack.
         * @param stream A transforming path stream.
         */
        unshiftPostProjected(stream) {
            this.postStack.unshift(stream);
        }
        /**
         * Removes the bottom-most path stream from the post-projected stack. The removed stream will have its consumer set
         * to {@link NullPathStream.INSTANCE}.
         * @returns The removed path stream, or undefined if this stack was empty.
         */
        shiftPostProjected() {
            return this.postStack.shift();
        }
        /** @inheritdoc */
        setConsumer(consumer) {
            this.postStack.setConsumer(consumer);
            super.setConsumer(consumer);
        }
        /** @inheritdoc */
        beginPath() {
            this.preStack.beginPath();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            this.preStack.moveTo(x, y);
        }
        /** @inheritdoc */
        lineTo(x, y) {
            this.preStack.lineTo(x, y);
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            this.preStack.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            this.preStack.quadraticCurveTo(cpx, cpy, x, y);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            this.preStack.arc(x, y, radius, startAngle, endAngle, counterClockwise);
        }
        /** @inheritdoc */
        closePath() {
            this.preStack.closePath();
        }
    }

    /**
     * An airspace renderer which does not draw any graphics.
     */
    class NullAirspaceRenderer {
        /** @inheritdoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        render(airspace, projection, context, lod = 0, stream) {
            // noop
        }
    }
    /**
     * An abstract implementation of MapAirspaceRenderer.
     */
    class MapAbstractAirspaceRenderer {
        /** @inheritdoc */
        render(airspace, projection, context, lod = 0, stream) {
            const shapes = airspace.lods[lod];
            const len = shapes.length;
            for (let i = 0; i < len; i++) {
                this.renderShape(shapes[i], projection, context, stream);
            }
        }
    }

    /**
     * A pipe from an input subscribable map to an output mutable subscribable map. Each key-value pair
     * added/changed/removed notification received by the pipe is used to add/change/remove key-value pairs in the output
     * map.
     */
    class SubscribableMapPipe extends HandlerSubscription {
        /**
         * Constructor.
         * @param from The input subscribable map.
         * @param to The output mutable subscribable map.
         * @param onDestroy A function which is called when this subscription is destroyed.
         */
        constructor(from, to, onDestroy) {
            const handler = (map, type, key, value) => {
                if (type === exports.SubscribableMapEventType.Deleted) {
                    to.delete(key);
                }
                else {
                    to.setValue(key, value);
                }
            };
            const initialNotifyFunc = () => {
                const fromMap = from.get();
                for (const key of to.get().keys()) {
                    if (!fromMap.has(key)) {
                        to.delete(key);
                    }
                }
                for (const [key, value] of fromMap) {
                    to.setValue(key, value);
                }
            };
            super(handler, initialNotifyFunc, onDestroy);
        }
    }

    /**
     * An abstract implementation of a subscribable set which allows adding, removing, and notifying subscribers.
     */
    class AbstractSubscribableMap {
        constructor() {
            this.isSubscribable = true;
            this.isSubscribableMap = true;
            this.notifyDepth = 0;
            /** A function which sends initial notifications to subscriptions. */
            this.initialNotifyFunc = this.initialNotify.bind(this);
            /** A function which responds to when a subscription to this subscribable is destroyed. */
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
        }
        /** @inheritdoc */
        get size() {
            return this.get().size;
        }
        /**
         * Adds a subscription to this map.
         * @param sub The subscription to add.
         */
        addSubscription(sub) {
            if (this.subs) {
                this.subs.push(sub);
            }
            else if (this.singletonSub) {
                this.subs = [this.singletonSub, sub];
                delete this.singletonSub;
            }
            else {
                this.singletonSub = sub;
            }
        }
        /** @inheritdoc */
        has(key) {
            return this.get().has(key);
        }
        /** @inheritdoc */
        getValue(key) {
            return this.get().get(key);
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Notifies subscriptions of a change in this map.
         * @param type The type of change.
         * @param key The key related to the change.
         * @param value The value related to the change.
         */
        notify(type, key, value) {
            const map = this.get();
            const canCleanUpSubs = this.notifyDepth === 0;
            let needCleanUpSubs = false;
            this.notifyDepth++;
            if (this.singletonSub) {
                try {
                    if (this.singletonSub.isAlive && !this.singletonSub.isPaused) {
                        this.singletonSub.handler(map, type, key, value);
                    }
                }
                catch (error) {
                    console.error(`AbstractSubscribableMap: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
                if (canCleanUpSubs) {
                    // If subscriptions were added during the notification, then singletonSub would be deleted and replaced with
                    // the subs array.
                    if (this.singletonSub) {
                        needCleanUpSubs = !this.singletonSub.isAlive;
                    }
                    else if (this.subs) {
                        for (let i = 0; i < this.subs.length; i++) {
                            if (!this.subs[i].isAlive) {
                                needCleanUpSubs = true;
                                break;
                            }
                        }
                    }
                }
            }
            else if (this.subs) {
                const subLen = this.subs.length;
                for (let i = 0; i < subLen; i++) {
                    try {
                        const sub = this.subs[i];
                        if (!sub.isPaused) {
                            sub.handler(map, type, key, value);
                        }
                        needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                    }
                    catch (error) {
                        console.error(`AbstractSubscribableMap: error in handler: ${error}`);
                        if (error instanceof Error) {
                            console.error(error.stack);
                        }
                    }
                }
                // If subscriptions were added during the notification and a cleanup operation is not already pending, then we
                // need to check if any of the new subscriptions are already dead and if so, pend a cleanup operation.
                if (canCleanUpSubs && !needCleanUpSubs) {
                    for (let i = subLen; i < this.subs.length; i++) {
                        if (!this.subs[i].isAlive) {
                            needCleanUpSubs = true;
                            break;
                        }
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs) {
                if (this.singletonSub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    this.subs = this.subs.filter(sub => sub.isAlive);
                }
            }
        }
        /**
         * Notifies a subscription of this map's current state.
         * @param sub The subscription to notify.
         */
        initialNotify(sub) {
            const map = this.get();
            for (const [key, value] of map) {
                sub.handler(map, exports.SubscribableMapEventType.Added, key, value);
            }
        }
        /**
         * Responds to when a subscription to this map is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                if (this.singletonSub === sub) {
                    delete this.singletonSub;
                }
                else if (this.subs) {
                    const index = this.subs.indexOf(sub);
                    if (index >= 0) {
                        this.subs.splice(index, 1);
                    }
                }
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        map(fn, equalityFunc, mutateFunc, initialVal) {
            const mapFunc = (inputs, previousVal) => fn(inputs[0], previousVal);
            return mutateFunc
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                ? MappedSubject.create(mapFunc, equalityFunc, mutateFunc, initialVal, this)
                : MappedSubject.create(mapFunc, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, this);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        pipe(to, arg2, arg3) {
            let sub;
            let paused;
            if (typeof arg2 === 'function') {
                sub = new SubscribablePipe(this, to, arg2, this.onSubDestroyedFunc);
                paused = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                if ('isSubscribableMap' in to) {
                    sub = new SubscribableMapPipe(this, to, this.onSubDestroyedFunc);
                }
                else {
                    sub = new SubscribablePipe(this, to, this.onSubDestroyedFunc);
                }
                paused = arg2 !== null && arg2 !== void 0 ? arg2 : false;
            }
            this.addSubscription(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
    }

    /**
     * A utility class for working with Accessibles.
     */
    class AccessibleUtils {
        /**
         * Checks if a query is an accessible.
         * @param query A query.
         * @returns Whether the query is an accessible.
         */
        static isAccessible(query) {
            if (typeof query === 'object' && query !== null) {
                return typeof query['get'] === 'function';
            }
            else {
                return false;
            }
        }
        /**
         * Checks if a query is a mutable accessible.
         * @param query A query.
         * @returns Whether the query is a mutable accessible.
         */
        static isMutableAccessible(query) {
            if (AccessibleUtils.isAccessible(query)) {
                return typeof query['set'] === 'function';
            }
            else {
                return false;
            }
        }
        /**
         * Converts a value to an accessible.
         *
         * If the `excludeAccessibles` argument is `true` and the value is already an accessible, then the value is returned
         * unchanged. Otherwise, a new accessible whose state is always equal to the value will be created and returned.
         * @param value The value to convert to an accessible.
         * @param excludeAccessibles Whether to return accessible values as-is instead of wrapping them in another
         * accessible.
         * @returns An accessible.
         */
        static toAccessible(value, excludeAccessibles) {
            if (excludeAccessibles && AccessibleUtils.isAccessible(value)) {
                return value;
            }
            else {
                return Value.create(value);
            }
        }
    }

    /**
     * A subscribable subject whose state is a combined tuple of an arbitrary number of values.
     * @deprecated This class has been deprecated in favor of using `MappedSubject` without an explicit mapping function.
     */
    class CombinedSubject extends AbstractSubscribable {
        /** @inheritdoc */
        get isAlive() {
            return this._isAlive;
        }
        /** @inheritdoc */
        get isPaused() {
            return this._isPaused;
        }
        /**
         * Constructor.
         * @param inputs The subscribables which provide the inputs to this subject.
         */
        constructor(...inputs) {
            super();
            /** @inheritdoc */
            this.canInitialNotify = true;
            this._isAlive = true;
            this._isPaused = false;
            this.inputs = inputs;
            this.inputValues = inputs.map(input => input.get());
            this.inputSubs = this.inputs.map((input, index) => input.sub(inputValue => {
                this.inputValues[index] = inputValue;
                this.notify();
            }));
        }
        /**
         * Creates a new subject whose state is a combined tuple of an arbitrary number of input values.
         * @param inputs The subscribables which provide the inputs to the new subject.
         * @returns A new subject whose state is a combined tuple of the specified input values.
         */
        static create(...inputs) {
            return new CombinedSubject(...inputs);
        }
        /** @inheritdoc */
        get() {
            return this.inputValues;
        }
        /** @inheritdoc */
        pause() {
            if (!this._isAlive) {
                throw new Error('CombinedSubject: cannot pause a dead subject');
            }
            if (this._isPaused) {
                return this;
            }
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputSubs[i].pause();
            }
            this._isPaused = true;
            return this;
        }
        /** @inheritdoc */
        resume() {
            if (!this._isAlive) {
                throw new Error('CombinedSubject: cannot resume a dead subject');
            }
            if (!this._isPaused) {
                return this;
            }
            this._isPaused = false;
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputValues[i] = this.inputs[i].get();
                this.inputSubs[i].resume();
            }
            this.notify();
            return this;
        }
        /** @inheritdoc */
        destroy() {
            this._isAlive = false;
            for (let i = 0; i < this.inputSubs.length; i++) {
                this.inputSubs[i].destroy();
            }
        }
    }

    /**
     * A class for subjects that return a computed value.
     * @class ComputedSubject
     * @template I The type of the input value.
     * @template T The type of the computed output value.
     */
    class ComputedSubject {
        /**
         * Creates an instance of ComputedSubject.
         * @param value The initial value.
         * @param computeFn The computation function.
         */
        constructor(value, computeFn) {
            this.computeFn = computeFn;
            this.isSubscribable = true;
            this.isMutableSubscribable = true;
            this.subs = [];
            this.notifyDepth = 0;
            this.initialNotifyFunc = this.notifySubscription.bind(this);
            this.onSubDestroyedFunc = this.onSubDestroyed.bind(this);
            this.rawValue = value;
            this.value = computeFn(value);
        }
        /**
         * Creates and returns a new ComputedSubject.
         * @param v The initial value of the Subject.
         * @param fn A function which transforms raw values to computed values.
         * @returns A ComputedSubject instance.
         */
        static create(v, fn) {
            return new ComputedSubject(v, fn);
        }
        /**
         * Sets the new value and notifies the subscribers when value changed.
         * @param value The new value.
         */
        set(value) {
            this.rawValue = value;
            const compValue = this.computeFn(value);
            if (compValue !== this.value) {
                this.value = compValue;
                this.notify();
            }
        }
        /**
         * Gets the computed value of the Subject.
         * @returns The computed value.
         */
        get() {
            return this.value;
        }
        /**
         * Gets the raw value of the Subject.
         * @returns The raw value.
         */
        getRaw() {
            return this.rawValue;
        }
        /** @inheritdoc */
        sub(handler, initialNotify = false, paused = false) {
            const sub = new HandlerSubscription(handler, this.initialNotifyFunc, this.onSubDestroyedFunc);
            this.subs.push(sub);
            if (paused) {
                sub.pause();
            }
            else if (initialNotify) {
                sub.initialNotify();
            }
            return sub;
        }
        /**
         * Notifies subscriptions that this subject's value has changed.
         */
        notify() {
            let needCleanUpSubs = false;
            this.notifyDepth++;
            const subLen = this.subs.length;
            for (let i = 0; i < subLen; i++) {
                try {
                    const sub = this.subs[i];
                    if (!sub.isPaused) {
                        this.notifySubscription(sub);
                    }
                    needCleanUpSubs || (needCleanUpSubs = !sub.isAlive);
                }
                catch (error) {
                    console.error(`ComputedSubject: error in handler: ${error}`);
                    if (error instanceof Error) {
                        console.error(error.stack);
                    }
                }
            }
            this.notifyDepth--;
            if (needCleanUpSubs && this.notifyDepth === 0) {
                this.subs = this.subs.filter(sub => sub.isAlive);
            }
        }
        /**
         * Notifies a subscription of this subject's current state.
         * @param sub The subscription to notify.
         */
        notifySubscription(sub) {
            sub.handler(this.value, this.rawValue);
        }
        /**
         * Responds to when a subscription to this subject is destroyed.
         * @param sub The destroyed subscription.
         */
        onSubDestroyed(sub) {
            // If we are not in the middle of a notify operation, remove the subscription.
            // Otherwise, do nothing and let the post-notify clean-up code handle it.
            if (this.notifyDepth === 0) {
                this.subs.splice(this.subs.indexOf(sub), 1);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        map(fn, equalityFunc, mutateFunc, initialVal) {
            const mapFunc = (inputs, previousVal) => fn(inputs[0], previousVal);
            return mutateFunc
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                ? MappedSubject.create(mapFunc, equalityFunc, mutateFunc, initialVal, this)
                : MappedSubject.create(mapFunc, equalityFunc !== null && equalityFunc !== void 0 ? equalityFunc : AbstractSubscribable.DEFAULT_EQUALITY_FUNC, this);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        pipe(to, arg2, arg3) {
            let sub;
            let paused;
            if (typeof arg2 === 'function') {
                sub = new SubscribablePipe(this, to, arg2, this.onSubDestroyedFunc);
                paused = arg3 !== null && arg3 !== void 0 ? arg3 : false;
            }
            else {
                sub = new SubscribablePipe(this, to, this.onSubDestroyedFunc);
                paused = arg2 !== null && arg2 !== void 0 ? arg2 : false;
            }
            this.subs.push(sub);
            if (paused) {
                sub.pause();
            }
            else {
                sub.initialNotify();
            }
            return sub;
        }
    }

    /**
     * A subscribable map whose key-value pairs can be freely added and removed for a set of allowed keys.
     */
    class FilteredMapSubject extends AbstractSubscribableMap {
        /**
         * Constructor.
         * @param allowedKeys The allowed keys for the new map. If an allowed key is removed after the map is created, then that
         * key will be removed from the map.
         * @param initialEntries The key-value pairs initially contained in the new map. If not defined, then the new map
         * will initially be empty.
         */
        constructor(allowedKeys, initialEntries) {
            super();
            this.isMutableSubscribable = true;
            this.isMutableSubscribableMap = true;
            this.allowedKeys = SetSubject.create();
            if ('isSubscribableSet' in allowedKeys && allowedKeys.isSubscribableSet === true) {
                this.keysSub = allowedKeys.pipe(this.allowedKeys);
            }
            else if ('isSubscribable' in allowedKeys && allowedKeys.isSubscribable === true) {
                this.keysSub = allowedKeys.pipe(this.allowedKeys);
            }
            else {
                this.allowedKeys.set(allowedKeys);
            }
            this.backingMap = new Map(initialEntries === undefined ? undefined : Array.from(initialEntries).filter(([key]) => this.allowedKeys.has(key)));
            this.allowedKeys.sub(this.onAllowedKeysChanged.bind(this));
        }
        /**
         * Creates and returns a new FilteredMapSubject.
         * @param allowedKeys The allowed keys for the new map. If an allowed key is removed after the map is created, then that
         * key will be removed from the map.
         * @param initialEntries The key-value pairs initially contained in the new map. If not defined, then the new map
         * will initially be empty.
         * @returns A new FilteredMapSubject instance.
         */
        static create(allowedKeys, initialEntries) {
            return new FilteredMapSubject(allowedKeys, initialEntries);
        }
        /** @inheritdoc */
        get() {
            return this.backingMap;
        }
        /**
         * Sets the key-value pairs contained in this map.
         * @param entries The key-value pairs to set.
         */
        set(entries) {
            const toAdd = entries instanceof Map ? entries : new Map(entries);
            for (const key of this.backingMap.keys()) {
                if (!toAdd.has(key)) {
                    this.delete(key);
                }
            }
            for (const key of toAdd.keys()) {
                this.setValue(key, toAdd.get(key));
            }
        }
        /** @inheritdoc */
        setValue(key, value) {
            if (!this.allowedKeys.has(key)) {
                return this;
            }
            const hasKey = this.backingMap.has(key);
            const shouldNotify = !hasKey || this.backingMap.get(key) !== value;
            this.backingMap.set(key, value);
            if (shouldNotify) {
                this.notify(hasKey ? exports.SubscribableMapEventType.Changed : exports.SubscribableMapEventType.Added, key, value);
            }
            return this;
        }
        /** @inheritdoc */
        delete(key) {
            const value = this.backingMap.get(key);
            const wasDeleted = this.backingMap.delete(key);
            if (wasDeleted) {
                this.notify(exports.SubscribableMapEventType.Deleted, key, value);
            }
            return wasDeleted;
        }
        /** @inheritdoc */
        clear() {
            for (const [key, value] of this.backingMap) {
                this.backingMap.delete(key);
                this.notify(exports.SubscribableMapEventType.Deleted, key, value);
            }
        }
        /**
         * Responds to when one of this map's allowed keys is added or removed.
         * @param keys The set of this map's allowed keys.
         * @param type The type of change.
         * @param key The key that was added or removed.
         */
        onAllowedKeysChanged(keys, type, key) {
            if (type === exports.SubscribableSetEventType.Deleted) {
                this.delete(key);
            }
        }
        /**
         * Destroys this map.
         */
        destroy() {
            var _a;
            (_a = this.keysSub) === null || _a === void 0 ? void 0 : _a.destroy();
        }
    }

    /**
     * An accessible value that is mapped from one or more input accessibles.
     */
    class MappedValue {
        /**
         * Creates a new MappedValue.
         * @param mapFunc The function that maps this accessible's input values.
         * @param inputs The accessibles that provide the inputs to the new mapped accessible.
         */
        constructor(mapFunc, ...inputs) {
            this.mapFunc = mapFunc;
            this.inputs = inputs;
            this.inputValues = inputs.map(input => input.get());
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(...args) {
            let mapFunc;
            if (typeof args[0] === 'function') {
                // Mapping function was supplied.
                mapFunc = args.shift();
            }
            else {
                mapFunc = MappedValue.IDENTITY_MAP;
            }
            return new MappedValue(mapFunc, ...args);
        }
        /** @inheritDoc */
        get() {
            for (let i = 0; i < this.inputValues.length; i++) {
                this.inputValues[i] = this.inputs[i].get();
            }
            return this.mapFunc(this.inputValues);
        }
    }
    MappedValue.IDENTITY_MAP = (inputs) => inputs;

    /**
     * A subscribable map whose key-value pairs can be freely added and removed.
     */
    class MapSubject extends AbstractSubscribableMap {
        /**
         * Constructor.
         * @param initialEntries The key-value pairs initially contained in the new map. If not defined, then the new map
         * will initially be empty.
         */
        constructor(initialEntries) {
            super();
            this.isMutableSubscribable = true;
            this.isMutableSubscribableMap = true;
            this.backingMap = new Map(initialEntries);
        }
        /**
         * Creates and returns a new MapSubject.
         * @param initialEntries The key-value pairs initially contained in the new map. If not defined, then the new map
         * will initially be empty.
         * @returns A new MapSubject instance.
         */
        static create(initialEntries) {
            return new MapSubject(initialEntries);
        }
        /** @inheritdoc */
        get() {
            return this.backingMap;
        }
        /**
         * Sets the key-value pairs contained in this map.
         * @param entries The key-value pairs to set.
         */
        set(entries) {
            const toAdd = entries instanceof Map ? entries : new Map(entries);
            for (const key of this.backingMap.keys()) {
                if (!toAdd.has(key)) {
                    this.delete(key);
                }
            }
            for (const key of toAdd.keys()) {
                this.setValue(key, toAdd.get(key));
            }
        }
        /** @inheritdoc */
        setValue(key, value) {
            const hasKey = this.backingMap.has(key);
            const shouldNotify = !hasKey || this.backingMap.get(key) !== value;
            this.backingMap.set(key, value);
            if (shouldNotify) {
                this.notify(hasKey ? exports.SubscribableMapEventType.Changed : exports.SubscribableMapEventType.Added, key, value);
            }
            return this;
        }
        /** @inheritdoc */
        delete(key) {
            const value = this.backingMap.get(key);
            const wasDeleted = this.backingMap.delete(key);
            if (wasDeleted) {
                this.notify(exports.SubscribableMapEventType.Deleted, key, value);
            }
            return wasDeleted;
        }
        /** @inheritdoc */
        clear() {
            for (const [key, value] of this.backingMap) {
                this.backingMap.delete(key);
                this.notify(exports.SubscribableMapEventType.Deleted, key, value);
            }
        }
    }

    /**
     * A base component for map layers.
     */
    class MapLayer extends DisplayComponent {
        constructor() {
            super(...arguments);
            this._isVisible = true;
        }
        /**
         * Checks whether this layer is visible.
         * @returns whether this layer is visible.
         */
        isVisible() {
            return this._isVisible;
        }
        /**
         * Sets this layer's visibility.
         * @param val Whether this layer should be visible.
         */
        setVisible(val) {
            if (this._isVisible === val) {
                return;
            }
            this._isVisible = val;
            this.onVisibilityChanged(val);
        }
        /**
         * This method is called when this layer's visibility changes.
         * @param isVisible Whether the layer is now visible.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            // noop
        }
        /**
         * This method is called when this layer is attached to its parent map component.
         */
        onAttached() {
            // noop
        }
        /**
         * This method is called when this layer's parent map is woken.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this layer's parent map is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when the map projection changes.
         * @param mapProjection - this layer's map projection.
         * @param changeFlags The types of changes made to the projection.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onMapProjectionChanged(mapProjection, changeFlags) {
            // noop
        }
        /**
         * This method is called once every map update cycle.
         * @param time The current time as a UNIX timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            // noop
        }
        /**
         * This method is called when this layer is detached from its parent map component.
         */
        onDetached() {
            // noop
        }
    }

    /**
     * The different types of map projection changes.
     */
    exports.MapProjectionChangeType = void 0;
    (function (MapProjectionChangeType) {
        MapProjectionChangeType[MapProjectionChangeType["Target"] = 1] = "Target";
        MapProjectionChangeType[MapProjectionChangeType["Center"] = 2] = "Center";
        MapProjectionChangeType[MapProjectionChangeType["TargetProjected"] = 4] = "TargetProjected";
        MapProjectionChangeType[MapProjectionChangeType["Range"] = 8] = "Range";
        MapProjectionChangeType[MapProjectionChangeType["RangeEndpoints"] = 16] = "RangeEndpoints";
        MapProjectionChangeType[MapProjectionChangeType["ScaleFactor"] = 32] = "ScaleFactor";
        MapProjectionChangeType[MapProjectionChangeType["Rotation"] = 64] = "Rotation";
        MapProjectionChangeType[MapProjectionChangeType["ProjectedSize"] = 128] = "ProjectedSize";
        MapProjectionChangeType[MapProjectionChangeType["ProjectedResolution"] = 256] = "ProjectedResolution";
    })(exports.MapProjectionChangeType || (exports.MapProjectionChangeType = {}));
    /**
     * A geographic projection model for a map. MapProjection uses a mercator projection.
     */
    class MapProjection {
        /**
         * Creates a new map projection.
         * @param projectedWidth The initial width of the projection window, in pixels.
         * @param projectedHeight The initial height of the projection window, in pixels.
         */
        constructor(projectedWidth, projectedHeight) {
            // settable parameters
            this.target = new GeoPoint(0, 0);
            this.targetProjectedOffset = new Float64Array(2);
            this.targetProjected = new Float64Array(2);
            this.range = 1; // great-arc radians
            this.rangeEndpoints = new Float64Array([0.5, 0, 0.5, 1]); // [relX1, relY1, relX2, relY2]
            this.projectedSize = new Float64Array(2);
            // computed parameters
            this.center = new GeoPoint(0, 0);
            this.centerProjected = new Float64Array(2);
            this.projectedRange = 0; // projected distance between the range endpoints in pixels
            this.widthRange = 0; // great-arc radians
            this.heightRange = 0; // great-arc radians
            this.oldParameters = {
                target: new GeoPoint(0, 0),
                center: new GeoPoint(0, 0),
                targetProjected: new Float64Array(2),
                range: 1,
                rangeEndpoints: new Float64Array(4),
                scaleFactor: 1,
                rotation: 0,
                projectedSize: new Float64Array(2),
                projectedResolution: 0
            };
            this.queuedParameters = Object.assign({}, this.oldParameters);
            this.updateQueued = false;
            this.changeListeners = [];
            Vec2Math.set(projectedWidth, projectedHeight, this.projectedSize);
            this.geoProjection = new MercatorProjection();
            Vec2Math.set(projectedWidth / 2, projectedHeight / 2, this.centerProjected);
            this.targetProjected.set(this.centerProjected);
            this.geoProjection.setReflectY(true).setTranslation(this.centerProjected);
            this.recompute();
        }
        /**
         * Gets this map projection's GeoProjection instance.
         * @returns This map projection's GeoProjection instance.
         */
        getGeoProjection() {
            return this.geoProjection;
        }
        /**
         * Gets the target geographic point of this projection. The target is guaranteed to be projected to a specific
         * point in the projected window defined by the center of the window plus the target projected offset.
         * @returns The target geographic point of this projection.
         */
        getTarget() {
            return this.target.readonly;
        }
        /**
         * Gets the projected offset from the center of the projected window of the target of this projection.
         * @returns The projected offset from the center of the projected window of the target of this projection.
         */
        getTargetProjectedOffset() {
            return this.targetProjectedOffset;
        }
        /**
         * Gets the projected location of the target of this projection.
         * @returns The projected location of the target of this projection.
         */
        getTargetProjected() {
            return this.targetProjected;
        }
        /**
         * Gets the range of this projection, in great-arc radians, as measured between the projection's two range endpoints.
         * @returns The range of this projection, in great-arc radians.
         */
        getRange() {
            return this.range;
        }
        /**
         * Gets the endpoints used to measure the range of the projection, as a 4-tuple `[relX1, relY1, relX2, relY2]`. Each
         * component is expressed in relative projected coordinates, where `0` is the left/top of the projected window, and
         * `1` is the right/bottom of the projected window.
         * @returns The endpoints used to measure the range of the projection, as a 4-tuple `[relX1, relY1, relX2, relY2]`.
         */
        getRangeEndpoints() {
            return this.rangeEndpoints;
        }
        /**
         * Gets the range of this projection, in great-arc radians, as measured from the center-left to the center-right of
         * the projected window.
         * @returns The range of this projection's projected window width, in great-arc radians.
         */
        getWidthRange() {
            return this.widthRange;
        }
        /**
         * Gets the range of this projection, in great-arc radians, as measured from the top-center to the bottom-center of
         * the projected window.
         * @returns The range of this projection's projected window height, in great-arc radians.
         */
        getHeightRange() {
            return this.heightRange;
        }
        /**
         * Gets the nominal scale factor of this projection. At a scale factor of 1, a distance of one great-arc radian will
         * be projected to a distance of one pixel.
         * @returns The nominal scale factor of this projection.
         */
        getScaleFactor() {
            return this.geoProjection.getScaleFactor();
        }
        /**
         * Gets the post-projected (planar) rotation angle of this projection in radians.
         * @returns The post-projected rotation angle of this projection.
         */
        getRotation() {
            return this.geoProjection.getPostRotation();
        }
        /**
         * Gets the size of the projected window, in pixels.
         * @returns The size of the projected window.
         */
        getProjectedSize() {
            return this.projectedSize;
        }
        /**
         * Gets the geographic point located at the center of this projection's projected window.
         * @returns The geographic point located at the center of this projection's projected window.
         */
        getCenter() {
            return this.center.readonly;
        }
        /**
         * Gets the center of this projection's projected window.
         * @returns The center of this projection's projected window.
         */
        getCenterProjected() {
            return this.centerProjected;
        }
        /**
         * Gets the average resolution, in great-arc radians per pixel, of the projected map along a line between the range
         * endpoints.
         * @returns The average resolution of the projected map along a line between the range endpoints.
         */
        getProjectedResolution() {
            return this.range / this.projectedRange;
        }
        /**
         * Calculates the true range of this projection, in great-arc radians, given a hypothetical projected center point.
         * @param centerProjected The projected location of the hypothetical center point to use for the calculation.
         * @returns The true range of this projection given the hypothetical projected center point.
         */
        calculateRangeAtCenter(centerProjected) {
            const endpoints = this.rangeEndpoints;
            const projectedWidth = this.projectedSize[0];
            const projectedHeight = this.projectedSize[1];
            const endpoint1 = MapProjection.tempVec2_3;
            endpoint1[0] = centerProjected[0] + projectedWidth * (endpoints[0] - 0.5);
            endpoint1[1] = centerProjected[1] + projectedHeight * (endpoints[1] - 0.5);
            const endpoint2 = MapProjection.tempVec2_4;
            endpoint2[0] = centerProjected[0] + projectedWidth * (endpoints[2] - 0.5);
            endpoint2[1] = centerProjected[1] + projectedHeight * (endpoints[3] - 0.5);
            const top = this.geoProjection.invert(endpoint1, MapProjection.tempGeoPoint_1);
            const bottom = this.geoProjection.invert(endpoint2, MapProjection.tempGeoPoint_2);
            return top.distance(bottom);
        }
        /**
         * Recomputes this projection's computed parameters.
         */
        recompute() {
            const currentTargetProjected = this.geoProjection.project(this.target, MapProjection.tempVec2_1);
            if (!isFinite(currentTargetProjected[0] + currentTargetProjected[1])) {
                // Check if we can potentially fix the geo projection by resetting its scale factor and center to defaults.
                const translation = this.geoProjection.getTranslation();
                if (isFinite(this.target.lat)
                    && isFinite(this.target.lon)
                    && isFinite(this.geoProjection.getPostRotation())
                    && isFinite(translation[0])
                    && isFinite(translation[1])) {
                    this.geoProjection.setScaleFactor(MapProjection.DEFAULT_SCALE_FACTOR);
                    this.geoProjection.setCenter(MapProjection.tempGeoPoint_1.set(0, 0));
                    this.geoProjection.setPreRotation(Vec3Math.set(0, 0, 0, MapProjection.vec3Cache[0]));
                }
                else {
                    return;
                }
            }
            const currentCenterProjected = MapProjection.tempVec2_2;
            currentCenterProjected.set(currentTargetProjected);
            currentCenterProjected[0] -= this.targetProjectedOffset[0];
            currentCenterProjected[1] -= this.targetProjectedOffset[1];
            let currentRange = this.calculateRangeAtCenter(currentCenterProjected);
            let ratio = currentRange / this.range;
            if (!isFinite(ratio) || ratio === 0) {
                return;
            }
            // iteratively find the appropriate scale factor (empiric testing shows this typically takes less than 4 iterations
            // to converge)
            let lastScaleFactor = this.geoProjection.getScaleFactor();
            let iterCount = 0;
            let ratioError = Math.abs(ratio - 1);
            let deltaRatioError = MapProjection.SCALE_FACTOR_TOLERANCE + 1;
            while (iterCount++ < MapProjection.SCALE_FACTOR_MAX_ITER
                && ratioError > MapProjection.SCALE_FACTOR_TOLERANCE
                && deltaRatioError > MapProjection.SCALE_FACTOR_TOLERANCE) {
                this.geoProjection.setScaleFactor(ratio * lastScaleFactor);
                this.geoProjection.project(this.target, currentTargetProjected);
                currentCenterProjected.set(currentTargetProjected);
                currentCenterProjected[0] -= this.targetProjectedOffset[0];
                currentCenterProjected[1] -= this.targetProjectedOffset[1];
                currentRange = this.calculateRangeAtCenter(currentCenterProjected);
                const newRatio = currentRange / this.range;
                const ratioDelta = newRatio - ratio;
                // Check to see if the ratio between current range and target range is invalid, did not change, or changed in the
                // direction opposite to what we were expecting. If so, this means that our range measurements are close to the
                // poles and range no longer monotonically increases with decreasing scale factor and vice versa. If we continue
                // iteration, we will likely push our scale factor to 0 or infinity. Therefore, we halt immediately and settle
                // for using the scale factor before we applied the most recent correction.
                if (!isFinite(ratio)
                    || ratio < 1 && ratioDelta <= 0
                    || ratio > 1 && ratioDelta >= 0) {
                    this.geoProjection.setScaleFactor(lastScaleFactor);
                    this.geoProjection.project(this.target, currentTargetProjected);
                    currentCenterProjected.set(currentTargetProjected);
                    currentCenterProjected[0] -= this.targetProjectedOffset[0];
                    currentCenterProjected[1] -= this.targetProjectedOffset[1];
                    break;
                }
                lastScaleFactor = this.geoProjection.getScaleFactor();
                ratio = newRatio;
                const newRatioError = Math.abs(ratio - 1);
                deltaRatioError = Math.abs(newRatioError - ratioError);
                ratioError = newRatioError;
            }
            // calculate the center point of the projection
            this.invert(currentCenterProjected, this.center);
            this.geoProjection.setCenter(this.center);
            // set the projection's pre-rotation to avoid anti-meridian wrapping issues
            const preRotation = Vec3Math.set(-this.center.lon * Avionics.Utils.DEG2RAD, 0, 0, MapProjection.vec3Cache[0]);
            this.geoProjection.setPreRotation(preRotation);
            const width = this.projectedSize[0];
            const height = this.projectedSize[1];
            this.projectedRange = Math.hypot((this.rangeEndpoints[2] - this.rangeEndpoints[0]) * width, (this.rangeEndpoints[3] - this.rangeEndpoints[1]) * height);
            const left = Vec2Math.set(0, height / 2, MapProjection.tempVec2_1);
            const right = Vec2Math.set(width, height / 2, MapProjection.tempVec2_2);
            this.widthRange = this.geoDistance(left, right);
            const top = Vec2Math.set(width / 2, 0, MapProjection.tempVec2_1);
            const bottom = Vec2Math.set(width / 2, height, MapProjection.tempVec2_2);
            this.heightRange = this.geoDistance(top, bottom);
        }
        /**
         * Sets this projection's parameters. Parameters not explicitly defined in the parameters argument will be left
         * unchanged.
         * @param parameters The new parameters.
         */
        set(parameters) {
            // save old values
            this.storeParameters(this.oldParameters);
            parameters.projectedSize && this.setProjectedSize(parameters.projectedSize);
            parameters.target && this.target.set(parameters.target);
            parameters.targetProjectedOffset && this.setTargetProjectedOffset(parameters.targetProjectedOffset);
            parameters.range !== undefined && (this.range = parameters.range);
            parameters.rangeEndpoints && this.rangeEndpoints.set(parameters.rangeEndpoints);
            parameters.rotation !== undefined && this.geoProjection.setPostRotation(parameters.rotation);
            let changeFlags = this.computeChangeFlags(this.oldParameters);
            if (changeFlags !== 0) {
                this.recompute();
                changeFlags |= this.computeDerivedChangeFlags(this.oldParameters);
                if (changeFlags !== 0) {
                    this.notifyChangeListeners(changeFlags);
                }
            }
        }
        /**
         * Sets the projection parameters to be applied when applyQueued() is called.
         * @param parameters The parameter changes to queue.
         */
        setQueued(parameters) {
            Object.assign(this.queuedParameters, parameters);
            this.updateQueued = true;
        }
        /**
         * Applies the set of queued projection changes, if any are queued.
         */
        applyQueued() {
            if (this.updateQueued) {
                this.updateQueued = false;
                this.set(this.queuedParameters);
                for (const key in this.queuedParameters) {
                    delete this.queuedParameters[key];
                }
            }
        }
        /**
         * Sets the size of the projected window.
         * @param size The new size, in pixels.
         */
        setProjectedSize(size) {
            this.projectedSize.set(size);
            Vec2Math.set(size[0] / 2, size[1] / 2, this.centerProjected);
            this.geoProjection.setTranslation(this.centerProjected);
            Vec2Math.add(this.centerProjected, this.targetProjectedOffset, this.targetProjected);
        }
        /**
         * Sets the projected offset from the center of the projected window of the target of this projection.
         * @param offset The new offset, in pixels.
         */
        setTargetProjectedOffset(offset) {
            this.targetProjectedOffset.set(offset);
            Vec2Math.add(this.centerProjected, this.targetProjectedOffset, this.targetProjected);
        }
        /**
         * Stores this projection's current parameters into a record.
         * @param record The record in which to store the parameters.
         */
        storeParameters(record) {
            record.target.set(this.target);
            record.center.set(this.center);
            record.targetProjected.set(this.targetProjected);
            record.range = this.range;
            record.rangeEndpoints.set(this.rangeEndpoints);
            record.scaleFactor = this.geoProjection.getScaleFactor();
            record.rotation = this.getRotation();
            record.projectedSize.set(this.projectedSize);
            record.projectedResolution = this.getProjectedResolution();
        }
        /**
         * Computes change flags given a set of old parameters.
         * @param oldParameters The old parameters.
         * @returns Change flags based on the specified old parameters.
         */
        computeChangeFlags(oldParameters) {
            return (oldParameters.target.equals(this.target) ? 0 : exports.MapProjectionChangeType.Target)
                | (Vec2Math.equals(oldParameters.targetProjected, this.targetProjected) ? 0 : exports.MapProjectionChangeType.TargetProjected)
                | (oldParameters.range === this.range ? 0 : exports.MapProjectionChangeType.Range)
                | (VecNMath.equals(oldParameters.rangeEndpoints, this.rangeEndpoints) ? 0 : exports.MapProjectionChangeType.RangeEndpoints)
                | (oldParameters.rotation === this.getRotation() ? 0 : exports.MapProjectionChangeType.Rotation)
                | (Vec2Math.equals(oldParameters.projectedSize, this.projectedSize) ? 0 : exports.MapProjectionChangeType.ProjectedSize);
        }
        /**
         * Computes change flags for derived parameters given a set of old parameters.
         * @param oldParameters The old parameters.
         * @returns Change flags for derived parameters based on the specified old parameters.
         */
        computeDerivedChangeFlags(oldParameters) {
            return (oldParameters.center.equals(this.center) ? 0 : exports.MapProjectionChangeType.Center)
                | (oldParameters.scaleFactor === this.geoProjection.getScaleFactor() ? 0 : exports.MapProjectionChangeType.ScaleFactor)
                | (oldParameters.projectedResolution === this.getProjectedResolution() ? 0 : exports.MapProjectionChangeType.ProjectedResolution);
        }
        /**
         * Projects a set of lat/lon coordinates.
         * @param point The point to project.
         * @param out The vector to which to write the result.
         * @returns The projected point, as a vector.
         */
        project(point, out) {
            return this.geoProjection.project(point, out);
        }
        /**
         * Inverts a set of projected coordinates. This method will determine the geographic point whose projected location
         * is the equal to that described by a 2D position vector.
         * @param vec The 2D position vector describing the location of the projected coordinates.
         * @param out The point to which to write the result.
         * @returns The inverted point.
         */
        invert(vec, out) {
            return this.geoProjection.invert(vec, out);
        }
        /**
         * Checks whether a point falls within certain projected bounds. The point can be specified as either a GeoPoint
         * object or a 2D vector. If a GeoPoint object is supplied, it will be projected before the bounds check takes
         * place.
         * @param point The point to check.
         * @param bounds The bounds to check against, expressed as a vector ([left, top, right, bottom]). Defaults to the
         * bounds of the projected window.
         * @returns Whether the point falls within the projected bounds.
         */
        isInProjectedBounds(point, bounds) {
            let left;
            let top;
            let right;
            let bottom;
            if (bounds) {
                left = bounds[0];
                top = bounds[1];
                right = bounds[2];
                bottom = bounds[3];
            }
            else {
                left = 0;
                top = 0;
                right = this.projectedSize[0];
                bottom = this.projectedSize[1];
            }
            if (!(point instanceof Float64Array)) {
                point = this.project(point, MapProjection.tempVec2_2);
            }
            const x = point[0];
            const y = point[1];
            return x >= left && x <= right && y >= top && y <= bottom;
        }
        /**
         * Gets the geographic great-circle distance between two points in great-arc radians. The points can be specified as
         * either GeoPoint objects or 2D vectors. If 2D vectors are supplied, they are interpreted as projected points and
         * inverse projection will be used to convert them to geographic points.
         * @param point1 The first point.
         * @param point2 The second point.
         * @returns The geographic great-circle distance between the points.
         */
        geoDistance(point1, point2) {
            if (point1 instanceof Float64Array) {
                point1 = this.invert(point1, MapProjection.tempGeoPoint_1);
            }
            if (point2 instanceof Float64Array) {
                point2 = this.invert(point2, MapProjection.tempGeoPoint_2);
            }
            return point1.distance(point2);
        }
        /**
         * Gets the projected Euclidean distance between two points in pixels. The points can be specified as either GeoPoint
         * objects or 2D vectors. If GeoPoint objects are supplied, they will be projected to convert them to projected
         * points.
         * @param point1 The first point.
         * @param point2 The second point.
         * @returns The projected Euclidean distance between two points.
         */
        projectedDistance(point1, point2) {
            if (!(point1 instanceof Float64Array)) {
                point1 = this.project(point1, MapProjection.tempVec2_1);
            }
            if (!(point2 instanceof Float64Array)) {
                point2 = this.project(point2, MapProjection.tempVec2_2);
            }
            return Vec2Math.distance(point1, point2);
        }
        /**
         * Notifies all registered change listeners that this projection has been changed.
         * @param changeFlags The types of changes that were made.
         */
        notifyChangeListeners(changeFlags) {
            for (let i = 0; i < this.changeListeners.length; i++) {
                this.changeListeners[i](this, changeFlags);
            }
        }
        /**
         * Registers a change listener with this projection. The listener will be called every time this projection changes.
         * A listener can be registered multiple times; it will be called once for every time it is registered.
         * @param listener The change listener to register.
         */
        addChangeListener(listener) {
            this.changeListeners.push(listener);
        }
        /**
         * Removes a change listener from this projection. If the specified listener was registered multiple times, this
         * method will only remove one instance of the listener.
         * @param listener The listener to remove.
         * @returns Whether the listener was successfully removed.
         */
        removeChangeListener(listener) {
            const index = this.changeListeners.lastIndexOf(listener);
            if (index >= 0) {
                this.changeListeners.splice(index, 1);
                return true;
            }
            else {
                return false;
            }
        }
    }
    MapProjection.DEFAULT_SCALE_FACTOR = UnitType.GA_RADIAN.convertTo(1, UnitType.NMILE);
    MapProjection.SCALE_FACTOR_MAX_ITER = 20;
    MapProjection.SCALE_FACTOR_TOLERANCE = 1e-6;
    MapProjection.tempVec2_1 = new Float64Array(2);
    MapProjection.tempVec2_2 = new Float64Array(2);
    MapProjection.tempVec2_3 = new Float64Array(2);
    MapProjection.tempVec2_4 = new Float64Array(2);
    MapProjection.tempGeoPoint_1 = new GeoPoint(0, 0);
    MapProjection.tempGeoPoint_2 = new GeoPoint(0, 0);
    MapProjection.vec3Cache = [Vec3Math.create()];

    /**
     * A component which displays a map. A map projects geographic coordinates onto a planar pixel grid. Each map component
     * maintains a MapProjection instance which handles the details of the projection. MapLayer objects added to the map
     * as children determine what is drawn on the map.
     */
    class MapComponent extends DisplayComponent {
        /** @inheritdoc */
        constructor(props) {
            var _a;
            super(props);
            this.layerEntries = [];
            this.lastUpdateTime = 0;
            this._isAwake = true;
            this.updateCycleHandler = this.update.bind(this);
            this.projectedSize = 'isSubscribable' in this.props.projectedSize ? this.props.projectedSize : Subject.create(this.props.projectedSize);
            const initialSize = this.projectedSize.get();
            if (this.props.projection !== undefined) {
                this.props.projection.set({ projectedSize: new Float64Array(initialSize) });
            }
            this.mapProjection = (_a = this.props.projection) !== null && _a !== void 0 ? _a : new MapProjection(initialSize[0], initialSize[1]);
        }
        /**
         * Gets the size of this map's projected window, in pixels.
         * @returns The size of this map's projected window.
         */
        getProjectedSize() {
            return this.mapProjection.getProjectedSize();
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether this map is awake.
         */
        get isAwake() {
            return this._isAwake;
        }
        /**
         * Puts this map to sleep. While asleep, this map will not be updated.
         */
        sleep() {
            this.setAwakeState(false);
        }
        /**
         * Wakes this map, allowing it to be updated.
         */
        wake() {
            this.setAwakeState(true);
        }
        /**
         * Sets this map's awake state. If the new awake state is the same as the current state, nothing will happen.
         * Otherwise, this map's layers will be notified that the map has either been woken or put to sleep.
         * @param isAwake The new awake state.
         */
        setAwakeState(isAwake) {
            if (this._isAwake === isAwake) {
                return;
            }
            this._isAwake = isAwake;
            this._isAwake ? this.onWake() : this.onSleep();
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            var _a;
            this.mapProjection.addChangeListener(this.onMapProjectionChanged.bind(this));
            this.projectedSizeSub = this.projectedSize.sub(size => {
                this.mapProjection.set({ projectedSize: size });
            });
            (_a = this.props.updateFreq) === null || _a === void 0 ? void 0 : _a.sub(freq => {
                var _a;
                (_a = this.updateCycleSub) === null || _a === void 0 ? void 0 : _a.destroy();
                this.updateCycleSub = this.props.bus.getSubscriber()
                    .on('realTime')
                    .whenChanged()
                    .atFrequency(freq)
                    .handle(this.updateCycleHandler);
            }, true);
            this.attachLayers(thisNode);
        }
        /**
         * Scans this component's VNode sub-tree for MapLayer components and attaches them when found. Only the top-most
         * level of MapLayer components are attached; layers that are themselves children of other layers are not attached.
         * @param thisNode This component's VNode.
         */
        attachLayers(thisNode) {
            exports.FSComponent.visitNodes(thisNode, node => {
                if (node.instance instanceof MapLayer) {
                    this.attachLayer(node.instance);
                    return true;
                }
                return false;
            });
        }
        /**
         * This method is called when the map is awakened.
         */
        onWake() {
            this.wakeLayers();
        }
        /**
         * Calls the onWake() method of this map's layers.
         */
        wakeLayers() {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onWake();
            }
        }
        /**
         * This method is called when the map is put to sleep.
         */
        onSleep() {
            this.sleepLayers();
        }
        /**
         * Calls the onSleep() method of this map's layers.
         */
        sleepLayers() {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onSleep();
            }
        }
        /**
         * This method is called when the map projection changes.
         * @param mapProjection This layer's map projection.
         * @param changeFlags The types of changes made to the projection.
         */
        onMapProjectionChanged(mapProjection, changeFlags) {
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.onProjectedSizeChanged();
            }
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].layer.onMapProjectionChanged(mapProjection, changeFlags);
            }
        }
        /**
         * Attaches a layer to this map component. If the layer is already attached, then this method has no effect.
         * @param layer The layer to attach.
         */
        attachLayer(layer) {
            if (this.layerEntries.findIndex(entry => entry.layer === layer) >= 0) {
                return;
            }
            const entry = new LayerEntry(layer);
            this.layerEntries.push(entry);
            entry.attach();
        }
        /**
         * Detaches a layer from this map component.
         * @param layer The layer to detach.
         * @returns Whether the layer was succesfully detached.
         */
        detachLayer(layer) {
            const index = this.layerEntries.findIndex(entry => entry.layer === layer);
            if (index >= 0) {
                const entry = this.layerEntries[index];
                entry.detach();
                this.layerEntries.splice(index, 1);
                return true;
            }
            else {
                return false;
            }
        }
        /**
         * Updates this map.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         */
        update(time) {
            if (!this._isAwake) {
                return;
            }
            this.onUpdated(time, time - this.lastUpdateTime);
            this.lastUpdateTime = time;
        }
        /**
         * This method is called once every update cycle.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        onUpdated(time, elapsed) {
            this.updateLayers(time, elapsed);
        }
        /**
         * Updates this map's attached layers.
         * @param time The current real time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        updateLayers(time, elapsed) {
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].update(time);
            }
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            super.destroy();
            (_a = this.updateCycleSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.projectedSizeSub) === null || _b === void 0 ? void 0 : _b.destroy();
            const len = this.layerEntries.length;
            for (let i = 0; i < len; i++) {
                this.layerEntries[i].destroy();
            }
        }
    }
    /**
     * An entry for a map layer.
     */
    class LayerEntry {
        /**
         * Constructor.
         * @param layer This entry's map layer.
         */
        constructor(layer) {
            this.layer = layer;
            this.updatePeriod = 0;
            this.lastUpdated = 0;
        }
        /**
         * Attaches this layer entry.
         */
        attach() {
            var _a, _b;
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.updateFreqSub = (_b = this.layer.props.updateFreq) === null || _b === void 0 ? void 0 : _b.sub((freq) => {
                const clamped = Math.max(0, freq);
                this.updatePeriod = clamped === 0 ? 0 : 1000 / clamped;
            }, true);
            this.layer.onAttached();
        }
        /**
         * Updates this layer entry.
         * @param currentTime The current time as a UNIX timestamp.
         */
        update(currentTime) {
            if (currentTime - this.lastUpdated >= this.updatePeriod) {
                this.layer.onUpdated(currentTime, currentTime - this.lastUpdated);
                this.lastUpdated = currentTime;
            }
        }
        /**
         * Detaches this layer entry.
         */
        detach() {
            var _a;
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.layer.onDetached();
        }
        /**
         * Destroys this layer entry. This will detach this entry's layer and destroy it.
         */
        destroy() {
            this.detach();
            this.layer.destroy();
        }
    }

    /**
     * An abstract implementation of a map text label.
     */
    class AbstractMapTextLabel {
        /**
         * Constructor.
         * @param text The text of this label, or a subscribable which provides it.
         * @param priority The render priority of this label, or a subscribable which provides it.
         * @param options Options with which to initialize this label.
         */
        constructor(text, priority, options) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
            this.text = SubscribableUtils.toSubscribable(text, true);
            this.priority = SubscribableUtils.toSubscribable(priority, true);
            this.anchor = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.anchor) !== null && _a !== void 0 ? _a : Vec2Math.create(), true);
            this.font = SubscribableUtils.toSubscribable((_b = options === null || options === void 0 ? void 0 : options.font) !== null && _b !== void 0 ? _b : '', true);
            this.fontSize = SubscribableUtils.toSubscribable((_c = options === null || options === void 0 ? void 0 : options.fontSize) !== null && _c !== void 0 ? _c : 10, true);
            this.fontStr = MappedSubject.create(([s, f]) => {
                return `${s}px ${f}`;
            }, this.fontSize, this.font);
            this.fontColor = SubscribableUtils.toSubscribable((_d = options === null || options === void 0 ? void 0 : options.fontColor) !== null && _d !== void 0 ? _d : 'white', true);
            this.fontOutlineWidth = SubscribableUtils.toSubscribable((_e = options === null || options === void 0 ? void 0 : options.fontOutlineWidth) !== null && _e !== void 0 ? _e : 0, true);
            this.fontOutlineColor = SubscribableUtils.toSubscribable((_f = options === null || options === void 0 ? void 0 : options.fontOutlineColor) !== null && _f !== void 0 ? _f : 'black', true);
            this.showBg = SubscribableUtils.toSubscribable((_g = options === null || options === void 0 ? void 0 : options.showBg) !== null && _g !== void 0 ? _g : false, true);
            this.bgColor = SubscribableUtils.toSubscribable((_h = options === null || options === void 0 ? void 0 : options.bgColor) !== null && _h !== void 0 ? _h : 'black', true);
            this.bgPadding = SubscribableUtils.toSubscribable((_j = options === null || options === void 0 ? void 0 : options.bgPadding) !== null && _j !== void 0 ? _j : VecNMath.create(4), true);
            this.bgBorderRadius = SubscribableUtils.toSubscribable((_k = options === null || options === void 0 ? void 0 : options.bgBorderRadius) !== null && _k !== void 0 ? _k : 0, true);
            this.bgOutlineWidth = SubscribableUtils.toSubscribable((_l = options === null || options === void 0 ? void 0 : options.bgOutlineWidth) !== null && _l !== void 0 ? _l : 0, true);
            this.bgOutlineColor = SubscribableUtils.toSubscribable((_m = options === null || options === void 0 ? void 0 : options.bgOutlineColor) !== null && _m !== void 0 ? _m : 'white', true);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        draw(context, mapProjection) {
            if (this.fontSize.get() !== 0) {
                this.setTextStyle(context);
                const width = context.measureText(this.text.get()).width;
                const height = this.fontSize.get();
                const showBg = this.showBg.get();
                const bgPadding = this.bgPadding.get();
                const bgOutlineWidth = this.bgOutlineWidth.get();
                const bgExtraWidth = showBg ? bgPadding[1] + bgPadding[3] + bgOutlineWidth * 2 : 0;
                const bgExtraHeight = showBg ? bgPadding[0] + bgPadding[2] + bgOutlineWidth * 2 : 0;
                const anchor = this.anchor.get();
                const pos = this.getPosition(mapProjection, AbstractMapTextLabel.tempVec2);
                const centerX = pos[0] - (anchor[0] - 0.5) * (width + bgExtraWidth);
                const centerY = pos[1] - (anchor[1] - 0.5) * (height + bgExtraHeight);
                if (showBg) {
                    this.drawBackground(context, centerX, centerY, width, height);
                }
                this.drawText(context, centerX, centerY);
            }
        }
        /**
         * Loads this label's text style to a canvas rendering context.
         * @param context The canvas rendering context to use.
         */
        setTextStyle(context) {
            context.font = this.fontStr.get();
            context.textBaseline = 'middle';
            context.textAlign = 'center';
        }
        /**
         * Draws this label's text to a canvas.
         * @param context The canvas rendering context.
         * @param centerX The x-coordinate of the center of the label, in pixels.
         * @param centerY the y-coordinate of the center of the label, in pixels.
         */
        drawText(context, centerX, centerY) {
            const text = this.text.get();
            const fontOutlineWidth = this.fontOutlineWidth.get();
            if (fontOutlineWidth > 0) {
                context.lineWidth = fontOutlineWidth * 2;
                context.strokeStyle = this.fontOutlineColor.get();
                context.strokeText(text, centerX, centerY);
            }
            context.fillStyle = this.fontColor.get();
            context.fillText(text, centerX, centerY);
        }
        /**
         * Draws this label's background to a canvas.
         * @param context The canvas rendering context.
         * @param centerX The x-coordinate of the center of the label, in pixels.
         * @param centerY the y-coordinate of the center of the label, in pixels.
         * @param width The width of the background, in pixels.
         * @param height The height of the background, in pixels.
         */
        drawBackground(context, centerX, centerY, width, height) {
            const bgPadding = this.bgPadding.get();
            const bgOutlineWidth = this.bgOutlineWidth.get();
            const bgBorderRadius = this.bgBorderRadius.get();
            const backgroundLeft = centerX - width / 2 - (bgPadding[3] + bgOutlineWidth);
            const backgroundTop = centerY - height / 2 - (bgPadding[0] + bgOutlineWidth);
            const backgroundWidth = width + (bgPadding[1] + bgPadding[3] + 2 * bgOutlineWidth);
            const backgroundHeight = height + (bgPadding[0] + bgPadding[2] + 2 * bgOutlineWidth);
            let isRounded = false;
            if (bgBorderRadius > 0) {
                isRounded = true;
                this.loadBackgroundPath(context, backgroundLeft, backgroundTop, backgroundWidth, backgroundHeight, bgBorderRadius);
            }
            if (bgOutlineWidth > 0) {
                context.lineWidth = bgOutlineWidth * 2;
                context.strokeStyle = this.bgOutlineColor.get();
                if (isRounded) {
                    context.stroke();
                }
                else {
                    context.strokeRect(backgroundLeft, backgroundTop, backgroundWidth, backgroundHeight);
                }
            }
            context.fillStyle = this.bgColor.get();
            if (isRounded) {
                context.fill();
            }
            else {
                context.fillRect(backgroundLeft, backgroundTop, backgroundWidth, backgroundHeight);
            }
        }
        /**
         * Loads the path of this label's background to a canvas rendering context.
         * @param context The canvas rendering context to use.
         * @param left The x-coordinate of the left edge of the background, in pixels.
         * @param top The y-coordinate of the top edge of the background, in pixels.
         * @param width The width of the background, in pixels.
         * @param height The height of the background, in pixels.
         * @param radius The border radius of the background, in pixels.
         */
        loadBackgroundPath(context, left, top, width, height, radius) {
            const right = left + width;
            const bottom = top + height;
            context.beginPath();
            context.moveTo(left + radius, top);
            context.lineTo(right - radius, top);
            context.arcTo(right, top, right, top + radius, radius);
            context.lineTo(right, bottom - radius);
            context.arcTo(right, bottom, right - radius, bottom, radius);
            context.lineTo(left + radius, bottom);
            context.arcTo(left, bottom, left, bottom - radius, radius);
            context.lineTo(left, top + radius);
            context.arcTo(left, top, left + radius, top, radius);
        }
    }
    AbstractMapTextLabel.tempVec2 = new Float64Array(2);
    /**
     * A text label associated with a specific geographic location.
     */
    class MapLocationTextLabel extends AbstractMapTextLabel {
        /**
         * Constructor.
         * @param text The text of this label, or a subscribable which provides it.
         * @param priority The render priority of this label, or a subscribable which provides it.
         * @param location The geographic location of this label, or a subscribable which provides it.
         * @param options Options with which to initialize this label.
         */
        constructor(text, priority, location, options) {
            var _a;
            super(text, priority, options);
            this.location = SubscribableUtils.toSubscribable(location, true);
            this.offset = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.offset) !== null && _a !== void 0 ? _a : Vec2Math.create(), true);
        }
        /** @inheritdoc */
        getPosition(mapProjection, out) {
            mapProjection.project(this.location.get(), out);
            Vec2Math.add(out, this.offset.get(), out);
            return out;
        }
    }

    /**
     * A cullable (hides labels that collide with other labels) text label associated with a specific geographic location.
     */
    class MapCullableLocationTextLabel extends MapLocationTextLabel {
        /**
         * Constructor.
         * @param text The text of this label, or a subscribable which provides it.
         * @param priority The priority of this label, or a subscribable which provides it.
         * @param location The geographic location of this label, or a subscribable which provides it.
         * @param alwaysShow Whether this label is immune to culling, or a subscribable which provides it.
         * @param options Options with which to initialize this label.
         */
        constructor(text, priority, location, alwaysShow, options) {
            super(text, priority, location, options);
            /** @inheritdoc */
            this.bounds = new Float64Array(4);
            /** @inheritdoc */
            this.invalidation = new SubEvent();
            this.subs = [];
            this.alwaysShow = SubscribableUtils.toSubscribable(alwaysShow, true);
            this.subs.push(this.priority.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.alwaysShow.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.location.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.text.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.fontSize.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.anchor.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.offset.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.bgPadding.sub(() => { this.invalidation.notify(this); }));
            this.subs.push(this.bgOutlineWidth.sub(() => { this.invalidation.notify(this); }));
        }
        /** @inheritdoc */
        updateBounds(mapProjection) {
            const fontSize = this.fontSize.get();
            const anchor = this.anchor.get();
            const width = 0.6 * fontSize * this.text.get().length;
            const height = fontSize;
            const pos = this.getPosition(mapProjection, MapCullableLocationTextLabel.tempVec2);
            let left = pos[0] - anchor[0] * width;
            let right = left + width;
            let top = pos[1] - anchor[1] * height;
            let bottom = top + height;
            if (this.showBg.get()) {
                const bgPadding = this.bgPadding.get();
                const bgOutlineWidth = this.bgOutlineWidth.get();
                left -= (bgPadding[3] + bgOutlineWidth);
                right += (bgPadding[1] + bgOutlineWidth);
                top -= (bgPadding[0] + bgOutlineWidth);
                bottom += (bgPadding[2] + bgOutlineWidth);
            }
            this.bounds[0] = left;
            this.bounds[1] = top;
            this.bounds[2] = right;
            this.bounds[3] = bottom;
        }
        /**
         * Destroys this label.
         */
        destroy() {
            for (const sub of this.subs) {
                sub.destroy();
            }
        }
    }
    /**
     * Manages a set of MapCullableTextLabels. Colliding labels will be culled based on their render priority. Labels with
     * lower priorities will be culled before labels with higher priorities.
     */
    class MapCullableTextLabelManager {
        // eslint-disable-next-line jsdoc/require-returns
        /** An array of labels registered with this manager that are visible. */
        get visibleLabels() {
            return this._visibleLabels;
        }
        /**
         * Creates an instance of the MapCullableTextLabelManager.
         * @param cullingEnabled Whether or not culling of labels is enabled.
         */
        constructor(cullingEnabled = true) {
            this.cullingEnabled = cullingEnabled;
            this.registered = new Map();
            this._visibleLabels = [];
            this.needUpdate = false;
            this.lastScaleFactor = 1;
            this.lastRotation = 0;
            this.invalidationHandler = () => { this.needUpdate = true; };
        }
        /**
         * Registers a label with this manager. Newly registered labels will be processed with the next manager update.
         * @param label The label to register.
         */
        register(label) {
            if (this.registered.has(label)) {
                return;
            }
            this.registered.set(label, label.invalidation.on(this.invalidationHandler));
            this.needUpdate = true;
        }
        /**
         * Deregisters a label with this manager. Newly deregistered labels will be processed with the next manager update.
         * @param label The label to deregister.
         */
        deregister(label) {
            const sub = this.registered.get(label);
            if (sub === undefined) {
                return;
            }
            sub.destroy();
            this.registered.delete(label);
            this.needUpdate = true;
        }
        /**
         * Sets whether or not text label culling is enabled.
         * @param enabled Whether or not culling is enabled.
         */
        setCullingEnabled(enabled) {
            this.cullingEnabled = enabled;
            this.needUpdate = true;
        }
        /**
         * Updates this manager.
         * @param mapProjection The projection of the map to which this manager's labels are to be drawn.
         */
        update(mapProjection) {
            if (!this.needUpdate) {
                const scaleFactorRatio = mapProjection.getScaleFactor() / this.lastScaleFactor;
                if (scaleFactorRatio < MapCullableTextLabelManager.SCALE_UPDATE_THRESHOLD && scaleFactorRatio > 1 / MapCullableTextLabelManager.SCALE_UPDATE_THRESHOLD) {
                    const rotationDelta = Math.abs(mapProjection.getRotation() - this.lastRotation);
                    if (Math.min(rotationDelta, 2 * Math.PI - rotationDelta) < MapCullableTextLabelManager.ROTATION_UPDATE_THRESHOLD) {
                        return;
                    }
                }
            }
            const labelArray = Array.from(this.registered.keys())
                .sort(MapCullableTextLabelManager.SORT_FUNC);
            if (this.cullingEnabled) {
                this._visibleLabels = [];
                const len = labelArray.length;
                for (let i = 0; i < len; i++) {
                    labelArray[i].updateBounds(mapProjection);
                }
                const collisionArray = [];
                for (let i = 0; i < len; i++) {
                    const label = labelArray[i];
                    let show = true;
                    if (!label.alwaysShow.get()) {
                        const len2 = collisionArray.length;
                        for (let j = 0; j < len2; j++) {
                            const other = collisionArray[j];
                            if (MapCullableTextLabelManager.doesCollide(label.bounds, other)) {
                                show = false;
                                break;
                            }
                        }
                    }
                    if (show) {
                        collisionArray.push(label.bounds);
                        this._visibleLabels.push(label);
                    }
                }
            }
            else {
                this._visibleLabels = labelArray;
            }
            this.lastScaleFactor = mapProjection.getScaleFactor();
            this.lastRotation = mapProjection.getRotation();
            this.needUpdate = false;
        }
        /**
         * Checks if two bounding boxes collide.
         * @param a The first bounding box, as a 4-tuple [left, top, right, bottom].
         * @param b The second bounding box, as a 4-tuple [left, top, right, bottom].
         * @returns whether the bounding boxes collide.
         */
        static doesCollide(a, b) {
            return a[0] < b[2]
                && a[2] > b[0]
                && a[1] < b[3]
                && a[3] > b[1];
        }
    }
    MapCullableTextLabelManager.SCALE_UPDATE_THRESHOLD = 1.2;
    MapCullableTextLabelManager.ROTATION_UPDATE_THRESHOLD = Math.PI / 6;
    MapCullableTextLabelManager.SORT_FUNC = (a, b) => {
        const alwaysShowA = a.alwaysShow.get();
        const alwaysShowB = b.alwaysShow.get();
        if (alwaysShowA && !alwaysShowB) {
            return -1;
        }
        else if (alwaysShowB && !alwaysShowA) {
            return 1;
        }
        else {
            return b.priority.get() - a.priority.get();
        }
    };

    /**
     * Calculates map projection parameters for fields of view which encompass sets of geographic points.
     */
    class MapFieldOfViewCalculator {
        /**
         * Constructor.
         * @param maxIter The maximum number of iterations to perform when calculating fields of view. Defaults to
         * {@link MapFieldOfViewCalculator.DEFAULT_MAX_ITER}.
         * @param rangeTolerance The error tolerance of calculated field of view ranges, as a fraction of the ideal range for
         * each field of view. Defaults to {@link MapFieldOfViewCalculator.DEFAULT_RANGE_TOLERANCE}.
         */
        constructor(maxIter = MapFieldOfViewCalculator.DEFAULT_MAX_ITER, rangeTolerance = MapFieldOfViewCalculator.DEFAULT_RANGE_TOLERANCE) {
            this.maxIter = maxIter;
            this.rangeTolerance = rangeTolerance;
            this.tempProjection = new MapProjection(100, 100);
        }
        /**
         * Calculates a map field of view, consisting of a range and target location, which encompasses a given set of
         * geographic points (the focus) with the smallest possible range. If there is only one point in the specified focus,
         * then the calculated range will be equal to 0. If the specified focus contains zero points or a field of view could
         * not be calculated, `NaN` will be written to the results.
         * @param mapProjection The projection of the map for which to calculate the field of view.
         * @param focus An array of points comprising the focus of the field of view.
         * @param margins The margins around the projected map boundaries to respect, as `[left, top, right, bottom]` in
         * pixels. The field of view will be calculated in order to avoid placing any points in the focus outside of the
         * margins.
         * @param out The object to which to write the results.
         * @returns The calculated field of view for the specified focus.
         */
        calculateFov(mapProjection, focus, margins, out) {
            out.range = NaN;
            out.target.set(NaN, NaN);
            if (focus.length === 0) {
                return out;
            }
            const projectedSize = mapProjection.getProjectedSize();
            const targetWidth = projectedSize[0] - margins[0] - margins[2];
            const targetHeight = projectedSize[1] - margins[1] - margins[3];
            if (targetWidth * targetHeight <= 0) {
                return out;
            }
            // Calculate mean point of the focus
            const mean = Vec3Math.set(0, 0, 0, MapFieldOfViewCalculator.vec3Cache[0]);
            for (let i = 0; i < focus.length; i++) {
                Vec3Math.add(mean, GeoPoint.sphericalToCartesian(focus[i], MapFieldOfViewCalculator.vec3Cache[1]), mean);
            }
            Vec3Math.multScalar(mean, 1 / focus.length, mean);
            // Initialize our working projection to use the same projected size, rotation, and range endpoints as the map
            // projection for which we are calculating the field of view.
            // Then, set the target offset of our working projection such that the target is projected to the middle of the
            // margin boundaries, and set the target to the mean focus point (this provides a rough estimate of the true center
            // of the focus when projected). Setting an initial target this way mitigates issues with anti-meridian wraparound.
            // Finally, use this projection to find the top-left and bottom-right corners of the projected focus, thus defining
            // the minimal axis-aligned bounding box of the projected focus.
            this.tempProjection.set({
                projectedSize: mapProjection.getProjectedSize(),
                rotation: mapProjection.getRotation(),
                target: MapFieldOfViewCalculator.geoPointCache[0].setFromCartesian(mean),
                targetProjectedOffset: Vec2Math.set(margins[0] + (targetWidth - projectedSize[0]) / 2, margins[1] + (targetHeight - projectedSize[1]) / 2, MapFieldOfViewCalculator.vec2Cache[0]),
                rangeEndpoints: mapProjection.getRangeEndpoints(),
                range: mapProjection.getRange()
            });
            let minX;
            let minY;
            let maxX;
            let maxY;
            for (let i = 0; i < focus.length; i++) {
                const projected = this.tempProjection.project(focus[i], MapFieldOfViewCalculator.vec2Cache[0]);
                minX = Math.min(projected[0], minX !== null && minX !== void 0 ? minX : Infinity);
                minY = Math.min(projected[1], minY !== null && minY !== void 0 ? minY : Infinity);
                maxX = Math.max(projected[0], maxX !== null && maxX !== void 0 ? maxX : -Infinity);
                maxY = Math.max(projected[1], maxY !== null && maxY !== void 0 ? maxY : -Infinity);
            }
            if (minX === undefined || minY === undefined || maxX === undefined || maxY === undefined) {
                return out;
            }
            let focusWidth = maxX - minX;
            let focusHeight = maxY - minY;
            if (focusWidth === 0 && focusHeight === 0) {
                out.target.set(focus[0]);
                out.range = 0;
                return out;
            }
            // Fix the target of our working projection (which we have already defined to be projected to the middle of the
            // margin boundaries) to the center of the focus. Due to the properties of the Mercator projection, this point is
            // invariant (it is always projected to the same coordinates) for every possible map range when selecting a field
            // of view that maximizes the distance between the bounding box of the projected focus and the margin boundaries.
            // Therefore, we will maintain this invariant while iteratively searching for the smallest map range that places
            // the focus bounding box within the margin boundaries.
            this.tempProjection.invert(Vec2Math.set((minX + maxX) / 2, (minY + maxY) / 2, MapFieldOfViewCalculator.vec2Cache[0]), out.target);
            this.tempProjection.set({
                target: out.target
            });
            let widthRatio = focusWidth / targetWidth;
            let heightRatio = focusHeight / targetHeight;
            let constrainedRatio = Math.max(widthRatio, heightRatio);
            const range = out.range = this.tempProjection.getRange();
            const topLeft = this.tempProjection.invert(Vec2Math.set(minX, minY, MapFieldOfViewCalculator.vec2Cache[0]), MapFieldOfViewCalculator.geoPointCache[0]);
            const bottomRight = this.tempProjection.invert(Vec2Math.set(maxX, maxY, MapFieldOfViewCalculator.vec2Cache[0]), MapFieldOfViewCalculator.geoPointCache[1]);
            let iterCount = 0;
            const rangeParam = { range };
            let ratioError = Math.abs(constrainedRatio - 1);
            let deltaRatioError = this.rangeTolerance + 1;
            while (iterCount++ < this.maxIter
                && ratioError > this.rangeTolerance
                && deltaRatioError > this.rangeTolerance) {
                rangeParam.range = out.range = this.tempProjection.getRange() * constrainedRatio;
                if (out.range <= GeoPoint.EQUALITY_TOLERANCE) {
                    // if the estimated range is too small, iteratively solving for the range will be unreliable due to floating
                    // point errors
                    out.range = GeoPoint.EQUALITY_TOLERANCE;
                    return out;
                }
                this.tempProjection.set(rangeParam);
                const topLeftProjected = this.tempProjection.project(topLeft, MapFieldOfViewCalculator.vec2Cache[0]);
                const bottomRightProjected = this.tempProjection.project(bottomRight, MapFieldOfViewCalculator.vec2Cache[1]);
                focusWidth = bottomRightProjected[0] - topLeftProjected[0];
                focusHeight = bottomRightProjected[1] - topLeftProjected[1];
                widthRatio = focusWidth / targetWidth;
                heightRatio = focusHeight / targetHeight;
                constrainedRatio = Math.max(widthRatio, heightRatio);
                const newRatioError = Math.abs(constrainedRatio - 1);
                deltaRatioError = Math.abs(newRatioError - ratioError);
                ratioError = newRatioError;
            }
            // Now that the appropriate field of view has been found using our working projection, back-calculate the map
            // target required to achieve this field of view in the map projection for which the field of view is being
            // calculated.
            this.tempProjection.invert(mapProjection.getTargetProjected(), out.target);
            return out;
        }
    }
    MapFieldOfViewCalculator.DEFAULT_MAX_ITER = 20;
    MapFieldOfViewCalculator.DEFAULT_RANGE_TOLERANCE = 0.01;
    MapFieldOfViewCalculator.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
    MapFieldOfViewCalculator.vec2Cache = [Vec2Math.create(), Vec2Math.create()];
    MapFieldOfViewCalculator.vec3Cache = [Vec3Math.create(), Vec3Math.create()];

    /**
     * A model for maps. Specific functionality is added by adding one or more modules to the model. Each module added to
     * the model is assigned a name which is used to retrieve it from the model.
     */
    class MapModel {
        constructor() {
            this.modules = new Map();
        }
        /**
         * Gets a module instance from the model and assigns it
         * to the provided type.
         * @param nameOrModule The module to get or the name of the module.
         * @returns The requested map data module.
         * @throws An error if
         */
        getModule(nameOrModule) {
            if (typeof nameOrModule === 'string') {
                return this.modules.get(nameOrModule);
            }
            else if (typeof nameOrModule === 'function') {
                return this.modules.get(nameOrModule.name);
            }
            throw new Error('Invalid type supplied: must be a string key or a module constructor.');
        }
        /**
         * Adds a module to this model.
         * @param name The name of the module to add.
         * @param module The module to add.
         */
        addModule(name, module) {
            if (this.modules.has(name)) {
                return;
            }
            this.modules.set(name, module);
        }
    }

    /**
     * An airspace renderer which supports rendering airspace borders as multiple, optionally offset lines.
     */
    class MapMultiLineAirspaceRenderer extends MapAbstractAirspaceRenderer {
        /** @inheritdoc */
        renderShape(shape, projection, context, stream) {
            var _a;
            if (shape.length < 2) {
                return;
            }
            const multiLineShape = (_a = MapMultiLineAirspaceRenderer.tempShape) !== null && _a !== void 0 ? _a : (MapMultiLineAirspaceRenderer.tempShape = new Shape());
            multiLineShape.build(shape, projection);
            this.renderLines(multiLineShape, context, stream);
        }
    }
    /**
     * An implementation of MapMultiLineAirspaceShape.
     */
    class Shape {
        constructor() {
            this.segments = [];
            this.length = 0;
            this.isClosed = false;
            this.windingOrder = 1;
        }
        /**
         * Builds this shape from a single contiguous airspace shape. Erases the current state of this shape.
         * @param shape An airspace shape.
         * @param projection The projection to use.
         */
        build(shape, projection) {
            if (shape.length < 2) {
                return;
            }
            this.buildSegments(shape, projection);
            // calculate winding order
            let signedArea = 0;
            let prev = this.segments[0];
            for (let i = 1; i < this.length; i++) {
                const segment = this.segments[i];
                signedArea += this.calculateSignedArea(segment, prev);
                prev = segment;
            }
            const first = this.segments[0].end;
            const last = this.segments[this.length - 1].end;
            this.isClosed = Vec2Math.equals(first, last);
            // if the path is not closed, we need to close the path to calculate a pseudo-winding order
            if (!this.isClosed) {
                signedArea += last[0] * first[1] - first[0] * last[1];
            }
            this.windingOrder = signedArea >= 0 ? 1 : -1;
            this.calculateVertices();
        }
        /**
         * Builds this shape's segments from a single contiguous airspace shape.
         * @param shape An airspace shape.
         * @param projection The projection to use.
         */
        buildSegments(shape, projection) {
            this.buildStartSegment(projection, 0, shape[0]);
            let start = shape[0].end;
            // project all points and calculate winding order
            this.length = shape.length;
            for (let i = 1; i < this.length; i++) {
                const vector = shape[i];
                this.buildSegment(projection, i, vector, start);
                start = vector.end;
            }
        }
        /**
         * Builds a single segment from an airspace shape vector.
         * @param projection The projection to use.
         * @param index The index of the segment to build.
         * @param vector An airspace shape vector.
         * @param start The start point of the airspace shape vector.
         */
        buildSegment(projection, index, vector, start) {
            const circle = vector.circle;
            if (circle) {
                if (circle.isGreatCircle()) {
                    this.buildSegmentFromGreatCircle(projection, index, circle, start, vector.end);
                }
                else {
                    this.buildSegmentFromSmallCircle(projection, index, circle, start, vector.end);
                }
            }
            else {
                this.buildStartSegment(projection, index, vector);
            }
        }
        /**
         * Builds a start segment from an airspace shape vector.
         * @param projection The projection to use.
         * @param index The index of the segment to build.
         * @param vector An airspace shape vector.
         */
        buildStartSegment(projection, index, vector) {
            var _a;
            var _b;
            const segment = ((_a = (_b = this.segments)[index]) !== null && _a !== void 0 ? _a : (_b[index] = Shape.createSegment()));
            segment.type = 'start';
            projection.project(vector.end, segment.end);
        }
        /**
         * Builds a segment from a great-circle path.
         * @param projection The projection to use.
         * @param index The index of the segment to build.
         * @param circle The great circle which defines the path.
         * @param start The start point of the path.
         * @param end The end point of the path.
         */
        buildSegmentFromGreatCircle(projection, index, circle, start, end) {
            var _a;
            var _b;
            const segment = ((_a = (_b = this.segments)[index]) !== null && _a !== void 0 ? _a : (_b[index] = Shape.createSegment()));
            segment.type = 'line';
            projection.project(end, segment.end);
            const delta = Vec2Math.sub(segment.end, this.segments[index - 1].end, Shape.vec2Cache[0]);
            Vec2Math.normalize(Vec2Math.normal(delta, segment.endNormal), segment.endNormal);
        }
        /**
         * Builds a segment from a small-circle path.
         * @param projection The projection to use.
         * @param index The index of the segment to build.
         * @param circle The small circle which defines the path.
         * @param start The start point of the path.
         * @param end The end point of the path.
         */
        buildSegmentFromSmallCircle(projection, index, circle, start, end) {
            var _a;
            var _b;
            const isClockwise = circle.radius > Math.PI / 2;
            const center = Shape.geoPointCache[0].setFromCartesian(isClockwise ? Vec3Math.multScalar(circle.center, -1, Shape.vec3Cache[0]) : circle.center);
            const startProjected = this.segments[index - 1].end;
            const centerProjected = projection.project(center, Shape.vec2Cache[0]);
            const endProjected = projection.project(end, Shape.vec2Cache[1]);
            const startRadial = Vec2Math.sub(startProjected, centerProjected, Shape.vec2Cache[2]);
            const startRadialMag = Vec2Math.abs(startRadial);
            const endRadial = Vec2Math.sub(endProjected, centerProjected, Shape.vec2Cache[3]);
            const endRadialMag = Vec2Math.abs(endRadial);
            const radius = (startRadialMag + endRadialMag) / 2;
            const segment = ((_a = (_b = this.segments)[index]) !== null && _a !== void 0 ? _a : (_b[index] = Shape.createSegment()));
            segment.type = 'arc';
            segment.radius = radius;
            segment.isClockwise = isClockwise;
            Vec2Math.copy(centerProjected, segment.center);
            Vec2Math.copy(endProjected, segment.end);
            Vec2Math.normalize(startRadial, segment.startNormal);
            Vec2Math.normalize(endRadial, segment.endNormal);
        }
        /**
         * Calculates the doubled signed area of a segment.
         * @param segment A segment.
         * @param prevSegment The segment immediately preceding `segment`.
         * @returns Twice the signed area of the segment.
         */
        calculateSignedArea(segment, prevSegment) {
            if (segment.type === 'start') {
                return 0;
            }
            if (segment.type === 'line') {
                return prevSegment.end[0] * segment.end[1] - segment.end[0] * prevSegment.end[1];
            }
            // arc
            const startRadial = Vec2Math.sub(prevSegment.end, segment.center, Shape.vec2Cache[0]);
            const startTheta = Vec2Math.theta(startRadial);
            const endRadial = Vec2Math.sub(segment.end, segment.center, Shape.vec2Cache[1]);
            const endTheta = Vec2Math.theta(endRadial);
            const angularWidth = Shape.getAngularWidth(startTheta, endTheta, segment.isClockwise, true);
            return prevSegment.end[0] * segment.center[1] - segment.center[0] * prevSegment.end[1]
                + segment.center[0] * segment.end[1] - segment.end[0] * segment.center[1]
                + angularWidth * segment.radius * segment.radius * (segment.isClockwise ? 1 : -1);
        }
        /**
         * Calculates vertex data for this shape.
         */
        calculateVertices() {
            const max = this.isClosed ? this.length - 1 : this.length;
            for (let i = 0; i < this.length; i++) {
                //const prev = isClosed ? (i + max - 2) % max + 1 : (i + max - 1) % max;
                const next = this.isClosed ? i % max + 1 : (i + 1) % max;
                this.calculateVertex(i, next);
            }
        }
        /**
         * Calculates data for a vertex.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         */
        calculateVertex(currIndex, nextIndex) {
            const curr = this.segments[currIndex];
            const next = this.segments[nextIndex];
            if (curr.type === 'start' && next.type === 'start') {
                curr.vertex.type = 'null';
                curr.vertex.fromIndex = currIndex;
                curr.vertex.toIndex = nextIndex;
            }
            else if (curr.type === 'start' && next.type === 'line') {
                this.calculateLineStartVertex(currIndex, nextIndex, true);
            }
            else if (curr.type === 'line' && next.type === 'start') {
                this.calculateLineStartVertex(currIndex, nextIndex, false);
            }
            else if (curr.type === 'start' && next.type === 'arc') {
                this.calculateArcStartVertex(currIndex, nextIndex, true);
            }
            else if (curr.type === 'arc' && next.type === 'start') {
                this.calculateArcStartVertex(currIndex, nextIndex, false);
            }
            else if (curr.type === 'line' && next.type === 'line') {
                this.calculateLineLineVertex(currIndex, nextIndex);
            }
            else if (curr.type === 'line' && next.type === 'arc') {
                this.calculateLineArcVertex(currIndex, nextIndex, false);
            }
            else if (curr.type === 'arc' && next.type === 'line') {
                this.calculateLineArcVertex(currIndex, nextIndex, true);
            }
            else {
                this.calculateArcArcVertex(currIndex, nextIndex);
            }
        }
        /**
         * Calculates data for a vertex connecting a line segment with a start segment.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         * @param isStartFirst Whether the start segment leads to the vertex.
         */
        calculateLineStartVertex(currIndex, nextIndex, isStartFirst) {
            const line = this.segments[isStartFirst ? nextIndex : currIndex];
            const vertex = this.segments[currIndex].vertex;
            vertex.type = 'line-start';
            vertex.fromIndex = currIndex;
            vertex.toIndex = nextIndex;
            Vec2Math.multScalar(line.endNormal, this.windingOrder, vertex.normal);
        }
        /**
         * Calculates data for a vertex connecting an arc segment with a start segment.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         * @param isStartFirst Whether the start segment leads to the vertex.
         */
        calculateArcStartVertex(currIndex, nextIndex, isStartFirst) {
            const arc = this.segments[isStartFirst ? nextIndex : currIndex];
            const vertex = this.segments[currIndex].vertex;
            vertex.type = 'arc-start';
            vertex.fromIndex = currIndex;
            vertex.toIndex = nextIndex;
            const arcNormal = isStartFirst ? arc.startNormal : arc.endNormal;
            Vec2Math.multScalar(arcNormal, this.windingOrder * (arc.isClockwise ? 1 : -1), vertex.normal);
        }
        /**
         * Calculates data for a vertex connecting two line segments.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         */
        calculateLineLineVertex(currIndex, nextIndex) {
            const curr = this.segments[currIndex];
            const next = this.segments[nextIndex];
            const vertex = curr.vertex;
            vertex.type = 'line-line';
            vertex.fromIndex = currIndex;
            vertex.toIndex = nextIndex;
            Vec2Math.normalize(Vec2Math.multScalar(Vec2Math.add(curr.endNormal, next.endNormal, vertex.normal), this.windingOrder, vertex.normal), vertex.normal);
            // scale the vertex normal unit vector to adjust for angle between the two joining lines (the closer the angle to
            // 0, the farther the point of intersection of offset lines lies to the vertex)
            const deltaUnit = Vec2Math.normalize(Vec2Math.sub(next.end, curr.end, Shape.vec2Cache[0]), Shape.vec2Cache[0]);
            const scaleFactor = Math.abs(1 / (vertex.normal[0] * deltaUnit[1] - vertex.normal[1] * deltaUnit[0]));
            Vec2Math.multScalar(vertex.normal, scaleFactor, vertex.normal);
        }
        /**
         * Calculates data for a vertex connecting an arc segment with an arc segment.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         * @param isArcFirst Whether the arc segment leads to the vertex.
         */
        calculateLineArcVertex(currIndex, nextIndex, isArcFirst) {
            const curr = this.segments[currIndex];
            const line = this.segments[isArcFirst ? nextIndex : currIndex];
            const arc = this.segments[isArcFirst ? currIndex : nextIndex];
            const end = curr.end;
            const vertex = curr.vertex;
            vertex.type = 'line-arc';
            vertex.fromIndex = currIndex;
            vertex.toIndex = nextIndex;
            vertex.arcOffsetSign = this.windingOrder * (arc.isClockwise ? 1 : -1);
            // find the transformation that translates the center of the arc to (0, 0) and rotates the line such that it is
            // parallel to the x-axis and places the intersection point in the positive x range.
            const centerEndDelta = Vec2Math.sub(isArcFirst ? arc.end : line.end, arc.center, Shape.vec2Cache[0]);
            const dot = centerEndDelta[1] * line.endNormal[0] - centerEndDelta[0] * line.endNormal[1];
            const theta = Vec2Math.theta(line.endNormal) + (dot >= 0 ? Math.PI / 2 : -Math.PI / 2);
            Shape.transformCache[0].toTranslation(-arc.center[0], -arc.center[1]);
            const rotation = Shape.transformCache[1].toRotation(-theta);
            Transform2D.concat(vertex.transform, Shape.transformCache);
            vertex.r0 = arc.radius;
            vertex.y0 = vertex.transform.apply(end, Shape.vec2Cache[0])[1];
            vertex.lineOffsetSign = (rotation.apply(line.endNormal, Shape.vec2Cache[0])[1] >= 0 ? this.windingOrder : -this.windingOrder);
            vertex.transform.invert();
        }
        /**
         * Calculates data for a vertex connecting two arc segments.
         * @param currIndex The index of the segment leading to the vertex to calculate.
         * @param nextIndex The index of the segment leading away from the vertex to calculate.
         */
        calculateArcArcVertex(currIndex, nextIndex) {
            const curr = this.segments[currIndex];
            const next = this.segments[nextIndex];
            const vertex = curr.vertex;
            vertex.type = 'arc-arc';
            vertex.fromIndex = currIndex;
            vertex.toIndex = nextIndex;
            // find the transformation that translates the center of the current arc to (0, 0) and rotates the next arc such
            // that its center lies on the x-axis and places the intersection point in the positive y range.
            const centerDelta = Vec2Math.sub(next.center, curr.center, Shape.vec2Cache[0]);
            const centerEndDelta = Vec2Math.sub(curr.end, curr.center, Shape.vec2Cache[1]);
            const dot = centerDelta[0] * centerEndDelta[1] - centerDelta[1] * centerEndDelta[0];
            const theta = Vec2Math.theta(centerDelta) + (dot >= 0 ? 0 : Math.PI);
            Shape.transformCache[0].toTranslation(-curr.center[0], -curr.center[1]);
            Shape.transformCache[1].toRotation(-theta);
            Transform2D.concat(vertex.transform, Shape.transformCache);
            vertex.d = vertex.transform.apply(next.center, Shape.vec2Cache[0])[0];
            vertex.arcOffsetSign = this.windingOrder * (curr.isClockwise ? 1 : -1);
            vertex.arc2OffsetSign = this.windingOrder * (next.isClockwise ? 1 : -1);
            vertex.transform.invert();
        }
        /** @inheritdoc */
        renderLine(context, offset, lineWidth, strokeStyle, dash, stream) {
            stream !== null && stream !== void 0 ? stream : (stream = context);
            stream.beginPath();
            const startPoint = Vec2Math.set(0, 0, Shape.vec2Cache[0]);
            if (this.isClosed) {
                // if the shape is closed, we need to initialize the first start point to the end point of the last segment
                if (offset === 0) {
                    Vec2Math.copy(this.segments[0].end, startPoint);
                }
                else {
                    const lastIndex = this.length - 1;
                    const result = this.calculateOffsetVertex(lastIndex, offset, startPoint);
                    if (!result || !Shape.isPointInSegmentBounds(this.segments[lastIndex], this.segments[lastIndex - 1].end, result)) {
                        this.calculateOffsetEndPoint(lastIndex, offset, startPoint);
                    }
                }
                stream.moveTo(startPoint[0], startPoint[1]);
            }
            for (let i = this.isClosed ? 1 : 0; i < this.length; i++) {
                const segment = this.segments[i];
                let newStartPoint;
                switch (segment.type) {
                    case 'start':
                        newStartPoint = this.pathStartSegment(stream, i, offset);
                        break;
                    case 'line':
                        newStartPoint = this.pathLineSegment(stream, i, startPoint, offset);
                        break;
                    case 'arc':
                        newStartPoint = this.pathArcSegment(stream, i, startPoint, offset);
                        break;
                }
                Vec2Math.copy(newStartPoint, startPoint);
            }
            context.lineWidth = lineWidth;
            context.strokeStyle = strokeStyle;
            context.setLineDash(dash);
            context.stroke();
        }
        /**
         * Loads a path for a start segment to a canvas rendering context.
         * @param stream The path stream to which to load the path.
         * @param index The index of the start segment.
         * @param offset The offset of the path from the segment, in pixels.
         * @returns The end point of the loaded path.
         */
        pathStartSegment(stream, index, offset) {
            const segment = this.segments[index];
            if (offset === 0) {
                stream.moveTo(segment.end[0], segment.end[1]);
                return segment.end;
            }
            else {
                const offsetEnd = this.calculateOffsetVertex(index, offset, Shape.vec2Cache[1]);
                if (offsetEnd) {
                    stream.moveTo(offsetEnd[0], offsetEnd[1]);
                    return offsetEnd;
                }
                else {
                    stream.moveTo(segment.end[0], segment.end[1]);
                    return segment.end;
                }
            }
        }
        /**
         * Loads a path for a line segment to a canvas rendering context.
         * @param stream The path stream to which to load the path.
         * @param index The index of the line segment.
         * @param start The start point of the line segment.
         * @param offset The offset of the path from the segment, in pixels.
         * @returns The end point of the loaded path.
         */
        pathLineSegment(stream, index, start, offset) {
            const segment = this.segments[index];
            if (offset !== 0 && !Shape.isPointInSegmentBounds(segment, this.segments[index - 1].end, start)) {
                const startPoint = this.calculateOffsetStartPoint(index, offset, Shape.vec2Cache[1]);
                stream.moveTo(startPoint[0], startPoint[1]);
            }
            if (offset === 0) {
                stream.lineTo(segment.end[0], segment.end[1]);
                return segment.end;
            }
            else {
                const offsetEnd = this.calculateOffsetVertex(index, offset, Shape.vec2Cache[1]);
                if (offsetEnd && Shape.isPointInSegmentBounds(segment, this.segments[index - 1].end, offsetEnd)) {
                    stream.lineTo(offsetEnd[0], offsetEnd[1]);
                    return offsetEnd;
                }
                else {
                    const endPoint = this.calculateOffsetEndPoint(index, offset, Shape.vec2Cache[1]);
                    stream.lineTo(endPoint[0], endPoint[1]);
                    const nextStartPoint = this.calculateOffsetStartPoint(segment.vertex.toIndex, offset, Shape.vec2Cache[1]);
                    stream.moveTo(nextStartPoint[0], nextStartPoint[1]);
                    return nextStartPoint;
                }
            }
        }
        /**
         * Loads a path for an arc segment to a canvas rendering context.
         * @param stream The path stream to which to load the path.
         * @param index The index of the arc segment.
         * @param start The start point of the arc segment.
         * @param offset The offset of the path from the segment, in pixels.
         * @returns The end point of the loaded path.
         */
        pathArcSegment(stream, index, start, offset) {
            const segment = this.segments[index];
            let startAngle;
            if (offset !== 0 && !Shape.isPointInSegmentBounds(segment, this.segments[index - 1].end, start)) {
                const startPoint = this.calculateOffsetStartPoint(index, offset, Shape.vec2Cache[1]);
                stream.moveTo(startPoint[0], startPoint[1]);
                startAngle = Math.atan2(startPoint[1] - segment.center[1], startPoint[0] - segment.center[0]);
            }
            else {
                startAngle = Math.atan2(start[1] - segment.center[1], start[0] - segment.center[0]);
            }
            if (offset === 0) {
                const endAngle = Vec2Math.theta(segment.endNormal);
                const angularWidth = Shape.getAngularWidth(startAngle, endAngle, segment.isClockwise, true);
                stream.arc(segment.center[0], segment.center[1], segment.radius, startAngle, startAngle + angularWidth * (segment.isClockwise ? 1 : -1), !segment.isClockwise);
                return segment.end;
            }
            else {
                const baseEndAngle = Vec2Math.theta(segment.endNormal);
                const baseAngularWidth = Shape.getAngularWidth(startAngle, baseEndAngle, segment.isClockwise, true);
                const sign = this.windingOrder * (segment.isClockwise ? 1 : -1);
                const radius = segment.radius + offset * sign;
                const offsetEnd = this.calculateOffsetVertex(index, offset, Shape.vec2Cache[1]);
                if (offsetEnd && Shape.isPointInSegmentBounds(segment, this.segments[index - 1].end, offsetEnd)) {
                    const endAngle = Math.atan2(offsetEnd[1] - segment.center[1], offsetEnd[0] - segment.center[0]);
                    const angularWidth = Shape.getAngularWidth(startAngle, endAngle, segment.isClockwise, true);
                    const angularWidthDiff = Math.abs(angularWidth - baseAngularWidth);
                    stream.arc(segment.center[0], segment.center[1], radius, startAngle, startAngle + angularWidth * (segment.isClockwise ? 1 : -1), angularWidthDiff < Math.PI ? !segment.isClockwise : segment.isClockwise);
                    return offsetEnd;
                }
                else {
                    if (radius > 0) {
                        const endPoint = this.calculateOffsetEndPoint(index, offset, Shape.vec2Cache[1]);
                        const endAngle = Math.atan2(endPoint[1] - segment.center[1], endPoint[0] - segment.center[0]);
                        const angularWidth = Shape.getAngularWidth(startAngle, endAngle, segment.isClockwise, true);
                        const angularWidthDiff = Math.abs(angularWidth - baseAngularWidth);
                        stream.arc(segment.center[0], segment.center[1], radius, startAngle, startAngle + angularWidth * (segment.isClockwise ? 1 : -1), angularWidthDiff < Math.PI ? !segment.isClockwise : segment.isClockwise);
                        const nextStartPoint = this.calculateOffsetStartPoint(segment.vertex.toIndex, offset, Shape.vec2Cache[1]);
                        stream.moveTo(nextStartPoint[0], nextStartPoint[1]);
                        return nextStartPoint;
                    }
                    else {
                        stream.moveTo(segment.center[0], segment.center[1]);
                        return segment.center;
                    }
                }
            }
        }
        /**
         * Calculates an offset point for a vertex.
         * @param index The index of the segment leading to the vertex to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset vertex point, or undefined if one could not be calculated.
         */
        calculateOffsetVertex(index, offset, out) {
            const vertex = this.segments[index].vertex;
            switch (vertex.type) {
                case 'line-start':
                case 'arc-start':
                case 'line-line':
                    return this.calculateOffsetVertexFromNormal(index, offset, out);
                case 'line-arc':
                    return this.calculateOffsetLineArcVertex(index, offset, out);
                case 'arc-arc':
                    return this.calculateOffsetArcArcVertex(index, offset, out);
                default:
                    return Vec2Math.copy(this.segments[vertex.fromIndex].end, out);
            }
        }
        /**
         * Calculates an offset point for a vertex which defines a normal vector.
         * @param index The index of the segment leading to the vertex to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset vertex point, or undefined if one could not be calculated.
         */
        calculateOffsetVertexFromNormal(index, offset, out) {
            const segment = this.segments[index];
            const vertex = segment.vertex;
            return Vec2Math.add(segment.end, Vec2Math.multScalar(vertex.normal, offset, out), out);
        }
        /**
         * Calculates an offset point for a vertex which connects a line segment with an arc segment.
         * @param index The index of the segment leading to the vertex to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset vertex point, or undefined if one could not be calculated.
         */
        calculateOffsetLineArcVertex(index, offset, out) {
            const vertex = this.segments[index].vertex;
            const radius = vertex.r0 + offset * vertex.arcOffsetSign;
            if (radius <= 0) {
                return undefined;
            }
            const y = vertex.y0 + offset * vertex.lineOffsetSign;
            const xSq = radius * radius - y * y;
            if (xSq < 0) {
                return undefined;
            }
            const x = Math.sqrt(xSq);
            return vertex.transform.apply(Vec2Math.set(x, y, out), out);
        }
        /**
         * Calculates an offset point for a vertex which connects two arc segments.
         * @param index The index of the segment leading to the vertex to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset vertex point, or undefined if one could not be calculated.
         */
        calculateOffsetArcArcVertex(index, offset, out) {
            const vertex = this.segments[index].vertex;
            if (vertex.d === 0) {
                return undefined;
            }
            const segment1 = this.segments[vertex.fromIndex];
            const segment2 = this.segments[vertex.toIndex];
            const radius1 = segment1.radius + offset * vertex.arcOffsetSign;
            const radius2 = segment2.radius + offset * vertex.arc2OffsetSign;
            if (radius1 <= 0 || radius2 <= 0) {
                return undefined;
            }
            const dSq = vertex.d * vertex.d;
            const radius1Sq = radius1 * radius1;
            const radius2Sq = radius2 * radius2;
            const x = (dSq - radius2Sq + radius1Sq) / (2 * vertex.d);
            const ySq = radius1Sq - x * x;
            if (ySq < 0) {
                return undefined;
            }
            const y = Math.sqrt(ySq);
            return vertex.transform.apply(Vec2Math.set(x, y, out), out);
        }
        /**
         * Calculates an offset start point for a segment.
         * @param index The index of the segment to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset start point.
         */
        calculateOffsetStartPoint(index, offset, out) {
            const prevSegment = this.segments[index - 1];
            const segment = this.segments[index];
            switch (segment.type) {
                case 'start':
                    return Vec2Math.copy(segment.end, out);
                case 'line':
                    return Vec2Math.set(prevSegment.end[0] + segment.endNormal[0] * this.windingOrder * offset, prevSegment.end[1] + segment.endNormal[1] * this.windingOrder * offset, out);
                case 'arc': {
                    const sign = this.windingOrder * (segment.isClockwise ? 1 : -1);
                    return Vec2Math.set(prevSegment.end[0] + segment.startNormal[0] * sign * offset, prevSegment.end[1] + segment.startNormal[1] * sign * offset, out);
                }
            }
        }
        /**
         * Calculates an offset end point for a segment.
         * @param index The index of the segment to offset.
         * @param offset The offset to apply, in pixels.
         * @param out The vector to which to write the result.
         * @returns The offset end point.
         */
        calculateOffsetEndPoint(index, offset, out) {
            const segment = this.segments[index];
            switch (segment.type) {
                case 'start':
                    return Vec2Math.copy(segment.end, out);
                case 'line':
                    return Vec2Math.set(segment.end[0] + segment.endNormal[0] * this.windingOrder * offset, segment.end[1] + segment.endNormal[1] * this.windingOrder * offset, out);
                case 'arc': {
                    const sign = this.windingOrder * (segment.isClockwise ? 1 : -1);
                    return Vec2Math.set(segment.end[0] + segment.endNormal[0] * sign * offset, segment.end[1] + segment.endNormal[1] * sign * offset, out);
                }
            }
        }
        /**
         * Checks whether a point lies within the bounds of a segment. If the segment is a start segment, all points are
         * considered to be in bounds. If the segment is a line or arc segment, a point is in bounds if and only if its
         * projection on the line or arc lies within the start and end points of the segment.
         * @param segment A segment.
         * @param start The start point of the segment.
         * @param point The query point.
         * @returns Whether the point lies within the bounds of the segment.
         */
        static isPointInSegmentBounds(segment, start, point) {
            if (segment.type === 'start') {
                return true;
            }
            if (segment.type === 'line') {
                const lineDeltaX = segment.end[0] - start[0];
                const lineDeltaY = segment.end[1] - start[1];
                const startDeltaX = point[0] - start[0];
                const startDeltaY = point[1] - start[1];
                const endDeltaX = point[0] - segment.end[0];
                const endDeltaY = point[1] - segment.end[1];
                const startDot = startDeltaX * lineDeltaX + startDeltaY * lineDeltaY;
                const endDot = endDeltaX * lineDeltaX + endDeltaY * lineDeltaY;
                return startDot >= 0 && endDot <= 0;
            }
            // arc
            const startAngle = Vec2Math.theta(segment.startNormal);
            const endAngle = Vec2Math.theta(segment.endNormal);
            const angularWidth = Shape.getAngularWidth(startAngle, endAngle, segment.isClockwise, true);
            const pointAngle = Math.atan2(point[1] - segment.center[1], point[0] - segment.center[0]);
            const angleDiff = Shape.getAngularWidth(startAngle, pointAngle, segment.isClockwise);
            return angleDiff <= angularWidth;
        }
        /**
         * Gets the angle subtended by an arc.
         * @param startAngle The radial angle of the arc's start point.
         * @param endAngle The radial angle of the arc's end point.
         * @param isClockwise Whether the arc runs clockwise.
         * @param zeroIsCircle Whether to consider a zero-angular-width arc to be a full circle. False by default.
         * @param zeroTolerance The tolerance of the zero-angular-width check used when `zeroIsCircle` is true. Defaults to
         * 1e-6.
         * @returns The angle subtended by the arc, in radians.
         */
        static getAngularWidth(startAngle, endAngle, isClockwise, zeroIsCircle = false, zeroTolerance = 1e-6) {
            const width = ((isClockwise ? endAngle - startAngle : startAngle - endAngle) + 2 * Math.PI) % (2 * Math.PI);
            return zeroIsCircle && Math.min(width, 2 * Math.PI - width) < zeroTolerance ? 2 * Math.PI : width;
        }
        /**
         * Creates a new segment.
         * @returns A new segment.
         */
        static createSegment() {
            return {
                type: 'arc',
                center: new Float64Array(2),
                radius: 0,
                isClockwise: false,
                end: new Float64Array(2),
                endNormal: new Float64Array(2),
                startNormal: new Float64Array(2),
                vertex: {
                    type: 'null',
                    fromIndex: -1,
                    toIndex: -1,
                    normal: new Float64Array(2),
                    transform: new Transform2D(),
                    r0: 0,
                    y0: 0,
                    lineOffsetSign: 1,
                    arcOffsetSign: 1,
                    arc2OffsetSign: 1,
                    d: 0
                }
            };
        }
    }
    Shape.geoPointCache = [new GeoPoint(0, 0)];
    Shape.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2), new Float64Array(2)];
    Shape.vec3Cache = [new Float64Array(3)];
    Shape.transformCache = [new Transform2D(), new Transform2D()];

    /**
     * An airspace renderer which renders airspace borders as a single line.
     */
    class MapSingleLineAirspaceRenderer extends MapAbstractAirspaceRenderer {
        /**
         * Constructor.
         * @param lineWidth The stroke width of the rendered airspace line.
         * @param strokeStyle The stroke style of the rendered airspace line.
         * @param dash The dash of the rendered airspace line.
         */
        constructor(lineWidth, strokeStyle, dash) {
            super();
            this.lineWidth = lineWidth;
            this.strokeStyle = strokeStyle;
            this.dash = dash;
        }
        /** @inheritdoc */
        renderShape(shape, projection, context, stream) {
            if (shape.length < 2) {
                return;
            }
            stream !== null && stream !== void 0 ? stream : (stream = context);
            stream.beginPath();
            const firstProjected = projection.project(shape[0].end, MapSingleLineAirspaceRenderer.vec2Cache[0]);
            stream.moveTo(firstProjected[0], firstProjected[1]);
            let start = shape[0].end;
            const len = shape.length;
            for (let i = 1; i < len; i++) {
                const vector = shape[i];
                const circle = vector.circle;
                if (circle) {
                    if (circle.isGreatCircle()) {
                        this.pathGreatCircle(circle, start, vector.end, projection, stream);
                    }
                    else {
                        this.pathSmallCircle(circle, start, vector.end, projection, stream);
                    }
                }
                else {
                    const endProjected = projection.project(vector.end, MapSingleLineAirspaceRenderer.vec2Cache[0]);
                    stream.moveTo(endProjected[0], endProjected[1]);
                }
                start = vector.end;
            }
            context.lineWidth = this.lineWidth;
            context.strokeStyle = this.strokeStyle;
            context.setLineDash(this.dash);
            context.stroke();
        }
        /**
         * Loads a projection of a great-circle path into a canvas rendering context.
         * @param circle The great circle defining the path.
         * @param start The start point of the path.
         * @param end The end point of the path.
         * @param projection The projection to use.
         * @param stream The path stream to which to load the projected path.
         */
        pathGreatCircle(circle, start, end, projection, stream) {
            const endProjected = projection.project(end, MapSingleLineAirspaceRenderer.vec2Cache[0]);
            stream.lineTo(endProjected[0], endProjected[1]);
        }
        /**
         * Loads a projection of a small-circle path into a canvas rendering context.
         * @param circle The small circle defining the path.
         * @param start The start point of the path.
         * @param end The end point of the path.
         * @param projection The projection to use.
         * @param stream The path stream to which to load the projected path.
         */
        pathSmallCircle(circle, start, end, projection, stream) {
            const center = MapSingleLineAirspaceRenderer.geoPointCache[0].setFromCartesian(circle.radius < Math.PI / 2 ? circle.center : Vec3Math.multScalar(circle.center, -1, MapSingleLineAirspaceRenderer.vec3Cache[0]));
            const centerProjected = projection.project(center, MapSingleLineAirspaceRenderer.vec2Cache[0]);
            const endProjected = projection.project(end, MapSingleLineAirspaceRenderer.vec2Cache[1]);
            if (start.equals(end)) {
                // draw a circle
                const radius = Vec2Math.distance(endProjected, centerProjected);
                const startAngle = Math.atan2(endProjected[1] - centerProjected[1], endProjected[0] - centerProjected[0]);
                stream.arc(centerProjected[0], centerProjected[1], radius, startAngle, startAngle + 2 * Math.PI);
                stream.moveTo(endProjected[0], endProjected[1]);
            }
            else {
                const startProjected = projection.project(start, MapSingleLineAirspaceRenderer.vec2Cache[2]);
                const startDelta = Vec2Math.sub(startProjected, centerProjected, MapSingleLineAirspaceRenderer.vec2Cache[3]);
                const startDeltaMag = Vec2Math.abs(startDelta);
                const endDelta = Vec2Math.sub(endProjected, centerProjected, MapSingleLineAirspaceRenderer.vec2Cache[4]);
                const endDeltaMag = Vec2Math.abs(endDelta);
                const radius = (startDeltaMag + endDeltaMag) / 2;
                const startAngle = Vec2Math.theta(startDelta);
                const arcStartX = centerProjected[0] + radius / startDeltaMag * startDelta[0];
                const arcStartY = centerProjected[1] + radius / startDeltaMag * startDelta[1];
                const endAngle = Vec2Math.theta(endDelta);
                const arcEndX = centerProjected[0] + radius / endDeltaMag * endDelta[0];
                const arcEndY = centerProjected[1] + radius / endDeltaMag * endDelta[1];
                stream.lineTo(arcStartX, arcStartY);
                stream.arc(centerProjected[0], centerProjected[1], radius, startAngle, endAngle, circle.radius < Math.PI / 2);
                stream.lineTo(arcEndX, arcEndY);
            }
        }
    }
    MapSingleLineAirspaceRenderer.geoPointCache = [new GeoPoint(0, 0)];
    MapSingleLineAirspaceRenderer.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2), new Float64Array(2), new Float64Array(2)];
    MapSingleLineAirspaceRenderer.vec3Cache = [new Float64Array(3)];

    /**
     * A blank waypoint icon.
     */
    class MapBlankWaypointIcon {
        /**
         * Constructor.
         * @param waypoint The waypoint associated with this icon.
         * @param priority The render priority of this icon. Icons with higher priorities should be rendered above those
         * with lower priorities.
         */
        constructor(waypoint, priority) {
            this.waypoint = waypoint;
            this.priority = SubscribableUtils.toSubscribable(priority, true);
        }
        /**
         * Does nothing.
         */
        draw() {
            // noop
        }
    }
    /**
     * An abstract implementation of MapWaypointIcon which supports an arbitrary anchor point and offset.
     */
    class AbstractMapWaypointIcon {
        /**
         * Constructor.
         * @param waypoint The waypoint associated with this icon.
         * @param priority The render priority of this icon, or a subscribable which provides it. Icons with higher
         * priorities should be rendered above those with lower priorities.
         * @param size The size of this icon, as `[width, height]` in pixels, or a subscribable which provides it.
         * @param options Options with which to initialize this icon.
         */
        constructor(waypoint, priority, size, options) {
            var _a, _b;
            this.waypoint = waypoint;
            this.priority = SubscribableUtils.toSubscribable(priority, true);
            this.size = SubscribableUtils.toSubscribable(size, true);
            this.anchor = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.anchor) !== null && _a !== void 0 ? _a : Vec2Math.create(0.5, 0.5), true);
            this.offset = SubscribableUtils.toSubscribable((_b = options === null || options === void 0 ? void 0 : options.offset) !== null && _b !== void 0 ? _b : Vec2Math.create(), true);
        }
        /** @inheritdoc */
        draw(context, mapProjection) {
            const size = this.size.get();
            const offset = this.offset.get();
            const anchor = this.anchor.get();
            const projected = mapProjection.project(this.waypoint.location.get(), MapWaypointImageIcon.tempVec2);
            const left = projected[0] + offset[0] - anchor[0] * size[0];
            const top = projected[1] + offset[1] - anchor[1] * size[1];
            this.drawIconAt(context, mapProjection, left, top);
        }
    }
    AbstractMapWaypointIcon.tempVec2 = new Float64Array(2);
    /**
     * A waypoint icon with an image as the icon's graphic source.
     */
    class MapWaypointImageIcon extends AbstractMapWaypointIcon {
        /**
         * Constructor.
         * @param waypoint The waypoint associated with this icon.
         * @param priority The render priority of this icon. Icons with higher priorities should be rendered above those
         * with lower priorities.
         * @param img This icon's image.
         * @param size The size of this icon, as `[width, height]` in pixels, or a subscribable which provides it.
         * @param options Options with which to initialize this icon.
         */
        constructor(waypoint, priority, img, size, options) {
            super(waypoint, priority, size, options);
            this.img = img;
        }
        /** @inheritdoc */
        drawIconAt(context, mapProjection, left, top) {
            const size = this.size.get();
            context.drawImage(this.img, left, top, size[0], size[1]);
        }
    }
    /**
     * A waypoint icon with a sprite as the icon's graphic source.
     */
    class MapWaypointSpriteIcon extends AbstractMapWaypointIcon {
        /**
         * Constructor.
         * @param waypoint The waypoint associated with this icon.
         * @param priority The render priority of this icon. Icons with higher priorities should be rendered above those
         * with lower priorities.
         * @param img This icon's sprite's image source.
         * @param frameWidth The frame width of the sprite, in pixels.
         * @param frameHeight The frame height of the sprite, in pixels.
         * @param size The size of this icon, as `[width, height]` in pixels, or a subscribable which provides it.
         * @param options Options with which to initialize this icon.
         * @param spriteFrameHandler An optional handler to determine the sprite frame to draw.
         */
        constructor(waypoint, priority, img, frameWidth, frameHeight, size, options, spriteFrameHandler) {
            super(waypoint, priority, size, options);
            this.img = img;
            this.frameWidth = frameWidth;
            this.frameHeight = frameHeight;
            this.spriteFrameHandler = spriteFrameHandler;
        }
        /** @inheritdoc */
        drawIconAt(context, mapProjection, left, top) {
            const size = this.size.get();
            const spriteIndex = this.getSpriteFrame(mapProjection);
            const rowCount = Math.floor(this.img.naturalHeight / this.frameHeight);
            const colCount = Math.floor(this.img.naturalWidth / this.frameWidth);
            const row = Math.min(rowCount - 1, Math.floor(spriteIndex / colCount));
            const col = Math.min(colCount - 1, spriteIndex % colCount);
            const spriteLeft = col * this.frameWidth;
            const spriteTop = row * this.frameHeight;
            context.drawImage(this.img, spriteLeft, spriteTop, this.frameWidth, this.frameHeight, left, top, size[0], size[1]);
        }
        /**
         * Gets the sprite frame to render.
         * @param mapProjection The map projection to use.
         * @returns The sprite frame to render.
         */
        getSpriteFrame(mapProjection) {
            if (this.spriteFrameHandler !== undefined) {
                return this.spriteFrameHandler(mapProjection);
            }
            return 0;
        }
    }

    /**
     * A renderer that draws waypoints to a map. For the renderer to draw a waypoint, the waypoint must first be registered
     * with the renderer. Waypoints may be registered under multiple render roles. Each render role is represented as a bit
     * flag. During each render cycle, a specific role is chosen for each waypoint by a selector function. Once the role is
     * chosen, the waypoint will be rendered in that role.
     */
    class MapWaypointRenderer {
        /**
         * Constructor.
         * @param textManager The text manager to use for waypoint labels.
         * @param selectRoleToRender A function which selects roles under which to render waypoints. Defaults to
         * {@link MapWaypointRenderer.DEFAULT_RENDER_ROLE_SELECTOR}.
         */
        constructor(textManager, selectRoleToRender = MapWaypointRenderer.DEFAULT_RENDER_ROLE_SELECTOR) {
            this.textManager = textManager;
            this.selectRoleToRender = selectRoleToRender;
            this.registered = new Map();
            this.toCleanUp = new Set();
            /**
             * This renderer's render role definitions. Waypoints assigned to be rendered under a role or combination of roles
             * with no definition will not be rendered.
             */
            this.roleDefinitions = new Map();
            /**
             * An event to subscribe to, fired when waypoints are added to the renderer.
             */
            this.onWaypointAdded = new SubEvent();
            /**
             * An event to subscribe to, fired when waypoints are removed from the render.
             */
            this.onWaypointRemoved = new SubEvent();
        }
        /**
         * Checks whether a render role has been added to this renderer.
         * @param role The render role to check.
         * @returns Whether the render role has been added to this renderer.
         */
        hasRenderRole(role) {
            return this.roleDefinitions.has(role);
        }
        /**
         * Adds a render role to this renderer. If the role has already been added to this renderer, this method does
         * nothing.
         * @param role The render role to add.
         * @param def The render role's definition. If undefined, the new role will be assigned a default definition with
         * no defined rendering context, icon, or label factories, and a visibility handler which always returns true.
         * @returns Whether the render role was successfully added.
         */
        addRenderRole(role, def) {
            if (this.roleDefinitions.has(role)) {
                return false;
            }
            this.roleDefinitions.set(role, Object.assign({}, def !== null && def !== void 0 ? def : MapWaypointRenderer.NULL_ROLE_DEF));
            return true;
        }
        /**
         * Removes a render role from this renderer.
         * @param role The render role to remove.
         * @returns Whether the render role was successfully removed.
         */
        removeRenderRole(role) {
            return this.roleDefinitions.delete(role);
        }
        /**
         * Gets the definition for a render role.
         * @param role A render role.
         * @returns The definition for the specified render role, or undefined if no such role has been added to this
         * renderer.
         */
        getRenderRoleDefinition(role) {
            return this.roleDefinitions.get(role);
        }
        /**
         * Gets an iterable of render roles added to this renderer. The iterable will return the roles in the order in which
         * they were added.
         * @returns An iterable of render roles added to this renderer.
         */
        renderRoles() {
            return this.roleDefinitions.keys();
        }
        /**
         * Removes all render roles from this renderer.
         */
        clearRenderRoles() {
            this.roleDefinitions.clear();
        }
        /**
         * Sets the factory to use to create waypoint icons for a render role. If the render role has not been added to this
         * renderer, this method does nothing.
         * @param role A render role.
         * @param factory A waypoint icon factory.
         * @returns Whether the factory was set.
         */
        setIconFactory(role, factory) {
            const roleDef = this.roleDefinitions.get(role);
            if (!roleDef) {
                return false;
            }
            roleDef.iconFactory = factory;
            return true;
        }
        /**
         * Sets the factory to use to create waypoint labels for a render role. If the render role has not been added to this
         * renderer, this method does nothing.
         * @param role A render role.
         * @param factory A waypoint label factory.
         * @returns Whether the factory was set.
         */
        setLabelFactory(role, factory) {
            const roleDef = this.roleDefinitions.get(role);
            if (!roleDef) {
                return false;
            }
            roleDef.labelFactory = factory;
            return true;
        }
        /**
         * Sets the canvas rendering context for a render role. If the render role has not been added to this renderer, this
         * method does nothing.
         * @param role A render role.
         * @param context A canvas 2D rendering context.
         * @returns Whether the context was set.
         */
        setCanvasContext(role, context) {
            const roleDef = this.roleDefinitions.get(role);
            if (!roleDef) {
                return false;
            }
            roleDef.canvasContext = context;
            return true;
        }
        /**
         * Sets the handler that determines if a waypoint should visible for a render role. If the render role has not been
         * added to this renderer, this method does nothing.
         * @param role A render role.
         * @param handler A function that determines if a waypoint should be visible.
         * @returns Whether the handler was set.
         */
        setVisibilityHandler(role, handler) {
            const roleDef = this.roleDefinitions.get(role);
            if (!roleDef) {
                return false;
            }
            roleDef.visibilityHandler = handler;
            return true;
        }
        /**
         * Checks if a waypoint is registered with this renderer. A role or roles can be optionally specified such that the
         * method will only return true if the waypoint is registered under those specific roles.
         * @param waypoint A waypoint.
         * @param role The specific role(s) to check.
         * @returns whether the waypoint is registered with this renderer.
         */
        isRegistered(waypoint, role) {
            if (!waypoint) {
                return false;
            }
            const entry = this.registered.get(waypoint.uid);
            if (!entry) {
                return false;
            }
            if (role === undefined) {
                return true;
            }
            return entry.isAllRoles(role);
        }
        /**
         * Registers a waypoint with this renderer under a specific role or roles. Registered waypoints will be drawn as
         * appropriate the next time this renderer's update() method is called. Registering a waypoint under a role under
         * which it is already registered has no effect unless the source of the registration is different.
         * @param waypoint The waypoint to register.
         * @param role The role(s) under which the waypoint should be registered.
         * @param sourceId A unique string ID for the source of the registration.
         */
        register(waypoint, role, sourceId) {
            if (role === 0 || sourceId === '') {
                return;
            }
            let entry = this.registered.get(waypoint.uid);
            if (!entry) {
                entry = new MapWaypointRendererEntry(waypoint, this.textManager, this.roleDefinitions, this.selectRoleToRender);
                this.registered.set(waypoint.uid, entry);
                this.onWaypointAdded.notify(this, waypoint);
            }
            entry.addRole(role, sourceId);
        }
        /**
         * Removes a registration for a waypoint for a specific role or roles. Once all of a waypoint's registrations for a
         * role are removed, it will no longer be rendered in that role the next this renderer's update() method is called.
         * @param waypoint The waypoint to deregister.
         * @param role The role(s) from which the waypoint should be deregistered.
         * @param sourceId The unique string ID for the source of the registration to remove.
         */
        deregister(waypoint, role, sourceId) {
            if (role === 0 || sourceId === '') {
                return;
            }
            const entry = this.registered.get(waypoint.uid);
            if (!entry) {
                return;
            }
            entry.removeRole(role, sourceId);
            if (entry.roles === 0) {
                this.deleteEntry(entry);
                this.onWaypointRemoved.notify(this, waypoint);
            }
        }
        /**
         * Deletes and cleans up a registered waypoint entry.
         * @param entry The entry to delete.
         */
        deleteEntry(entry) {
            this.registered.delete(entry.waypoint.uid);
            this.toCleanUp.add(entry);
        }
        /**
         * Redraws waypoints registered with this renderer.
         * @param mapProjection The map projection to use.
         */
        update(mapProjection) {
            var _a;
            this.toCleanUp.forEach(entry => {
                entry.destroy();
            });
            this.toCleanUp.clear();
            const entriesToDrawIcon = [];
            this.registered.forEach(entry => {
                entry.update();
                if (entry.icon) {
                    entriesToDrawIcon.push(entry);
                }
            });
            const projectedSize = mapProjection.getProjectedSize();
            for (const roleDef of this.roleDefinitions.values()) {
                const context = roleDef.canvasContext;
                if (context) {
                    context.clearRect(0, 0, projectedSize[0], projectedSize[1]);
                }
            }
            entriesToDrawIcon.sort(MapWaypointRenderer.ENTRY_SORT_FUNC);
            const len2 = entriesToDrawIcon.length;
            for (let i = 0; i < len2; i++) {
                const entry = entriesToDrawIcon[i];
                const icon = entry.icon;
                const context = (_a = this.roleDefinitions.get(entry.lastRenderedRole)) === null || _a === void 0 ? void 0 : _a.canvasContext;
                if (context) {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    icon.draw(context, mapProjection);
                }
            }
        }
        /**
         * Gets the nearest waypoint currently registered in the renderer.
         * @param pos The position to get the closest waypoint to.
         * @param first A predicate that will search the list of closest waypoints for a match, and return the first one found.
         * @returns The nearest waypoint, or undefined if none found.
         */
        getNearestWaypoint(pos, first) {
            var _a, _b;
            const ordered = [...this.registered.values()].sort((a, b) => this.orderByDistance(a.waypoint, b.waypoint, pos))
                .filter(w => {
                const roleDef = this.getRenderRoleDefinition(w.lastRenderedRole);
                if (roleDef !== undefined) {
                    return roleDef.visibilityHandler(w.waypoint);
                }
                return false;
            });
            if (first !== undefined) {
                return (_a = ordered.find(entry => first(entry.waypoint))) === null || _a === void 0 ? void 0 : _a.waypoint;
            }
            return (_b = ordered[0]) === null || _b === void 0 ? void 0 : _b.waypoint;
        }
        /**
         * Orders waypoints by their distance to the plane PPOS.
         * @param a The first waypoint.
         * @param b The second waypoint.
         * @param pos The position to compare against.
         * @returns The comparison order number.
         */
        orderByDistance(a, b, pos) {
            const aDist = a.location.get().distance(pos);
            const bDist = b.location.get().distance(pos);
            return aDist - bDist;
        }
    }
    /** A null render role definition. Icons rendered under this role are never visible. */
    MapWaypointRenderer.NULL_ROLE_DEF = {
        iconFactory: null,
        labelFactory: null,
        canvasContext: null,
        visibilityHandler: () => true
    };
    /**
     * Sorts waypoint entries such that those with icons of higher priority are sorted after those with icons of lower
     * priority.
     * @param a The first waypoint entry to sort.
     * @param b The second waypoint entry to sort.
     * @returns A negative number if the first entry is to be sorted before the second, a positive number if the second
     * entry is to be sorted before the first, and zero if the entries' relative sorting order does not matter.
     */
    MapWaypointRenderer.ENTRY_SORT_FUNC = (a, b) => {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return a.icon.priority.get() - b.icon.priority.get();
    };
    /**
     * The default render role selector. For each waypoint entry, iterates through all possible render roles in the order
     * they were originally added to the renderer and selects the first role under which the entry is registered and is
     * visible.
     * @param entry A waypoint entry.
     * @param roleDefinitions A map from all possible render roles to their definitions.
     * @returns The role under which the waypoint entry should be rendered, or 0 if the entry should not be rendered
     * under any role.
     */
    MapWaypointRenderer.DEFAULT_RENDER_ROLE_SELECTOR = (entry, roleDefinitions) => {
        for (const role of roleDefinitions.keys()) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            if (entry.isAllRoles(role) && roleDefinitions.get(role).visibilityHandler(entry.waypoint)) {
                return role;
            }
        }
        return 0;
    };
    /**
     * An entry for a waypoint registered with {@link MapWaypointRenderer}.
     */
    class MapWaypointRendererEntry {
        /**
         * Constructor.
         * @param waypoint The waypoint associated with this entry.
         * @param textManager The text manager to which to register this entry's labels.
         * @param roleDefinitions A map of all valid render roles to their definitions.
         * @param selectRoleToRender A function to use to select roles under which to render this entry.
         */
        constructor(waypoint, textManager, roleDefinitions, selectRoleToRender) {
            this.waypoint = waypoint;
            this.textManager = textManager;
            this.roleDefinitions = roleDefinitions;
            this.selectRoleToRender = selectRoleToRender;
            this.registrations = {};
            this._roles = 0;
            this._icon = null;
            this._label = null;
            this._lastRenderedRole = 0;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The render role(s) assigned to this entry. */
        get roles() {
            return this._roles;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The role under which this entry was last rendered, or 0 if this entry has not yet been rendered. */
        get lastRenderedRole() {
            return this._lastRenderedRole;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** This entry's waypoint icon. */
        get icon() {
            return this._icon;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** This entry's waypoint label. */
        get label() {
            return this._label;
        }
        /**
         * Checks whether this entry is assigned any of the specified render roles. Optionally, this method can also check
         * if this entry was last rendered in any of the specified roles instead.
         * @param roles The render roles against which to check.
         * @param useLastRendered Whether to check the role(s) in which this entry was last rendered instead of the current
         * roles assigned to this entry. False by default.
         * @returns whether the check passed.
         */
        isAnyRole(roles, useLastRendered = false) {
            let toCompare;
            if (useLastRendered) {
                toCompare = this.lastRenderedRole;
            }
            else {
                toCompare = this.roles;
            }
            return BitFlags.isAny(toCompare, roles);
        }
        /**
         * Checks whether this entry is assigned only the specified render role(s). Optionally, this method can also check
         * if this entry was last rendered in only the specified role(s) instead.
         * @param roles The render roles against which to check.
         * @param useLastRendered Whether to check the role(s) in which this entry was last rendered instead of the current
         * roles assigned to this entry. False by default.
         * @returns whether the check passed.
         */
        isOnlyRole(roles, useLastRendered = false) {
            let toCompare;
            if (useLastRendered) {
                toCompare = this.lastRenderedRole;
            }
            else {
                toCompare = this.roles;
            }
            return toCompare === roles;
        }
        /**
         * Checks whether this entry is assigned all the specified render role(s). Optionally, this method can also check
         * if this entry was last rendered in all the specified role(s) instead.
         * @param roles - the render role(s) against which to check.
         * @param useLastRendered Whether to check the role(s) in which this entry was last rendered instead of the current
         * roles assigned to this entry. False by default.
         * @returns whether the check passed.
         */
        isAllRoles(roles, useLastRendered = false) {
            let toCompare;
            if (useLastRendered) {
                toCompare = this.lastRenderedRole;
            }
            else {
                toCompare = this.roles;
            }
            return BitFlags.isAll(toCompare, roles);
        }
        /**
         * Assigns one or more render roles to this entry.
         * @param roles The render role(s) to assign.
         * @param sourceId The unique string ID of the source of the assignment.
         */
        addRole(roles, sourceId) {
            BitFlags.forEach(roles, (value, index) => {
                var _a;
                var _b, _c;
                ((_a = (_b = this.registrations)[_c = 1 << index]) !== null && _a !== void 0 ? _a : (_b[_c] = new Set())).add(sourceId);
            }, true);
            this._roles = this._roles | roles;
        }
        /**
         * Removes one or more render roles from this entry.
         * @param roles The render role(s) to remove.
         * @param sourceId The unique string ID of the soruce of the de-assignment.
         */
        removeRole(roles, sourceId) {
            BitFlags.forEach(roles, (value, index) => {
                const role = 1 << index;
                const registrations = this.registrations[role];
                if (registrations) {
                    registrations.delete(sourceId);
                    if (registrations.size === 0) {
                        this._roles = this._roles & ~role;
                    }
                }
            }, true);
        }
        /**
         * Prepares this entry for rendering.
         * @param showRole The role in which this entry should be rendered.
         * @param iconFactory The factory to use to get a waypoint icon.
         * @param labelFactory The factory to use to get a waypoint label.
         */
        prepareRender(showRole, iconFactory, labelFactory) {
            var _a, _b;
            if (showRole === this._lastRenderedRole) {
                return;
            }
            this._icon = (_a = iconFactory === null || iconFactory === void 0 ? void 0 : iconFactory.getIcon(showRole, this.waypoint)) !== null && _a !== void 0 ? _a : null;
            const label = (_b = labelFactory === null || labelFactory === void 0 ? void 0 : labelFactory.getLabel(showRole, this.waypoint)) !== null && _b !== void 0 ? _b : null;
            if (this._label && this._label !== label) {
                this.textManager.deregister(this._label);
            }
            if (label && label !== this._label) {
                this.textManager.register(label);
            }
            this._label = label;
            this._lastRenderedRole = showRole;
        }
        /**
         * Updates this entry. An appropriate render role is selected, then the icon and label are updated as appropriate
         * for the chosen role. If the waypoint's label should be visible, it is added to the appropriate text manager.
         * Of note, this method will not draw the waypoint icon to a canvas element; it will simply ensure the .showIcon
         * property contains the correct value depending on whether the icon should be visible.
         */
        update() {
            var _a, _b;
            const showRole = this.selectRoleToRender(this, this.roleDefinitions);
            const roleDef = this.roleDefinitions.get(showRole);
            const iconFactory = (_a = roleDef === null || roleDef === void 0 ? void 0 : roleDef.iconFactory) !== null && _a !== void 0 ? _a : null;
            const labelFactory = (_b = roleDef === null || roleDef === void 0 ? void 0 : roleDef.labelFactory) !== null && _b !== void 0 ? _b : null;
            this.prepareRender(showRole, iconFactory, labelFactory);
        }
        /**
         * Destroys this entry. Any label from this entry currently registered with the text manager will be deregistered.
         */
        destroy() {
            if (this._label) {
                this.textManager.deregister(this._label);
            }
        }
    }

    /**
     * A module describing the display of airspaces.
     */
    class MapAirspaceModule {
        /**
         * Constructor.
         * @param showTypes A map of this module's airspace show types to their associated nearest boundary search filter
         * bitflags.
         */
        constructor(showTypes) {
            this.showTypes = showTypes;
            this.show = {};
            for (const type in showTypes) {
                this.show[type] = Subject.create(false);
            }
        }
    }

    /**
     * A module describing the state of the autopilot.
     */
    class MapAutopilotPropsModule {
        constructor() {
            /** The altitude preselector setting. */
            this.selectedAltitude = NumberUnitSubject.create(UnitType.FOOT.createNumber(0));
            /** The selected heading setting, in degrees. */
            this.selectedHeading = Subject.create(0);
        }
    }

    /**
     * A map module describing whether or not various signals are valid.
     */
    class MapDataIntegrityModule {
        constructor() {
            /** Whether GPS position data is valid. */
            this.gpsSignalValid = Subject.create(false);
            /** Whether heading data is valid. */
            this.headingSignalValid = Subject.create(false);
            /** Whether attitude data is valid. */
            this.attitudeSignalValid = Subject.create(false);
            /** Whether ADC data is valid. */
            this.adcSignalValid = Subject.create(false);
        }
    }

    /**
     * A module describing the nominal range of a map.
     */
    class MapIndexedRangeModule {
        /** @inheritdoc */
        constructor() {
            /** The index of the nominal range. */
            this.nominalRangeIndex = Subject.create(0);
            /** The array of possible map nominal ranges. */
            this.nominalRanges = Subject.create([UnitType.NMILE.createNumber(1)]);
            /** The nominal range. */
            this.nominalRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(1));
            this.nominalRanges.sub(this.onNominalRangesChanged.bind(this));
        }
        /**
         * A callback which is called when the nominal range array changes.
         * @param array The new array.
         */
        onNominalRangesChanged(array) {
            const currentIndex = this.nominalRangeIndex.get();
            this.setNominalRangeIndex(Utils.Clamp(currentIndex, 0, array.length - 1));
        }
        /**
         * Sets the nominal range by index.
         * @param index The index of the new nominal range.
         * @returns The value of the new nominal range.
         * @throws Error if index of out of bounds.
         */
        setNominalRangeIndex(index) {
            const rangeArray = this.nominalRanges.get();
            if (index < 0 || index >= rangeArray.length) {
                throw new Error('Index out of bounds.');
            }
            const range = rangeArray[index];
            this.nominalRangeIndex.set(index);
            this.nominalRange.set(range);
            return range;
        }
    }

    /**
     * Map own airplane icon orientations.
     */
    exports.MapOwnAirplaneIconOrientation = void 0;
    (function (MapOwnAirplaneIconOrientation) {
        MapOwnAirplaneIconOrientation["HeadingUp"] = "HeadingUp";
        MapOwnAirplaneIconOrientation["TrackUp"] = "TrackUp";
        MapOwnAirplaneIconOrientation["MapUp"] = "MapUp";
    })(exports.MapOwnAirplaneIconOrientation || (exports.MapOwnAirplaneIconOrientation = {}));
    /**
     * A module describing properties of the own airplane icon.
     */
    class MapOwnAirplaneIconModule {
        constructor() {
            /** Whether to show the airplane icon. */
            this.show = Subject.create(true);
            /** The orientation of the airplane icon. */
            this.orientation = Subject.create(exports.MapOwnAirplaneIconOrientation.HeadingUp);
        }
    }

    /**
     * A module describing the state of the own airplane.
     */
    class MapOwnAirplanePropsModule {
        constructor() {
            /** The airplane's position. */
            this.position = GeoPointSubject.create(new GeoPoint(0, 0));
            /** The airplane's true heading, in degrees. */
            this.hdgTrue = Subject.create(0);
            /** The airplane's turn rate, in degrees per second. */
            this.turnRate = Subject.create(0);
            /** The airplane's indicated altitude. */
            this.altitude = NumberUnitSubject.create(UnitType.FOOT.createNumber(0));
            /** The airplane's vertical speed. */
            this.verticalSpeed = NumberUnitSubject.create(UnitType.FPM.createNumber(0));
            /** The airplane's true ground track, in degrees. */
            this.trackTrue = Subject.create(0);
            /** The airplane's ground speed. */
            this.groundSpeed = NumberUnitSubject.create(UnitType.KNOT.createNumber(0));
            /** Whether the airplane is on the ground. */
            this.isOnGround = Subject.create(true);
            /** The magnetic variation at the airplane's position. */
            this.magVar = Subject.create(0);
        }
    }

    /**
     * A module describing the nominal range of a map.
     */
    class MapRangeModule {
        constructor() {
            /** The range of the map as a number unit. */
            this.nominalRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(1));
        }
    }

    /**
     * An implementation of MapCanvasLayerCanvasInstance.
     */
    class MapCanvasLayerCanvasInstanceClass {
        /**
         * Creates a new canvas instance.
         * @param canvas The canvas element.
         * @param context The canvas 2D rendering context.
         * @param isDisplayed Whether the canvas is displayed.
         */
        constructor(canvas, context, isDisplayed) {
            this.canvas = canvas;
            this.context = context;
            this.isDisplayed = isDisplayed;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        clear() {
            this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        reset() {
            const width = this.canvas.width;
            this.canvas.width = 0;
            this.canvas.width = width;
        }
    }
    /**
     * A layer which uses a canvas to draw graphics.
     */
    class MapCanvasLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.displayCanvasRef = exports.FSComponent.createRef();
            this.width = 0;
            this.height = 0;
            this.displayCanvasContext = null;
            this.isInit = false;
        }
        /**
         * Gets this layer's display canvas instance.
         * @returns This layer's display canvas instance.
         * @throws Error if this layer's display canvas instance has not been initialized.
         */
        get display() {
            if (!this._display) {
                throw new Error('MapCanvasLayer: attempted to access display before it was initialized');
            }
            return this._display;
        }
        /**
         * Gets this layer's buffer canvas instance.
         * @returns This layer's buffer canvas instance.
         * @throws Error if this layer's buffer canvas instance has not been initialized.
         */
        get buffer() {
            if (!this._buffer) {
                throw new Error('MapCanvasLayer: attempted to access buffer before it was initialized');
            }
            return this._buffer;
        }
        /**
         * Attempts to get this layer's display canvas instance.
         * @returns This layer's display canvas instance, or undefined if it has not been initialized.
         */
        tryGetDisplay() {
            return this._display;
        }
        /**
         * Attempts to get this layer's buffer canvas instance.
         * @returns This layer's buffer canvas instance, or undefined if it has not been initialized.
         */
        tryGetBuffer() {
            return this._buffer;
        }
        /**
         * Gets the width of the canvas element, in pixels.
         * @returns the width of the canvas element.
         */
        getWidth() {
            return this.width;
        }
        /**
         * Gets the height of the canvas element, in pixels.
         * @returns the height of the canvas element.
         */
        getHeight() {
            return this.height;
        }
        /**
         * Sets the width of the canvas element, in pixels.
         * @param width The new width.
         */
        setWidth(width) {
            if (width === this.width) {
                return;
            }
            this.width = width;
            if (this.isInit) {
                this.updateCanvasSize();
            }
        }
        /**
         * Sets the height of the canvas element, in pixels.
         * @param height The new height.
         */
        setHeight(height) {
            if (height === this.height) {
                return;
            }
            this.height = height;
            if (this.isInit) {
                this.updateCanvasSize();
            }
        }
        /**
         * Copies the contents of the buffer to the display. Has no effect if this layer does not have a buffer.
         */
        copyBufferToDisplay() {
            if (!this.isInit || !this.props.useBuffer) {
                return;
            }
            this.display.context.drawImage(this.buffer.canvas, 0, 0, this.width, this.height);
        }
        /**
         * A callback called after the component renders.
         */
        onAfterRender() {
            this.displayCanvasContext = this.displayCanvasRef.instance.getContext('2d');
        }
        // eslint-disable-next-line jsdoc/require-jsdoc, @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            if (this.isInit) {
                this.updateCanvasVisibility();
            }
        }
        /**
         * Updates this layer according to its current visibility.
         */
        updateFromVisibility() {
            this.display.canvas.style.display = this.isVisible() ? 'block' : 'none';
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        onAttached() {
            this.initCanvasInstances();
            this.isInit = true;
            this.updateCanvasVisibility();
            this.updateCanvasSize();
        }
        /**
         * Initializes this layer's canvas instances.
         */
        initCanvasInstances() {
            this._display = this.createCanvasInstance(this.displayCanvasRef.instance, this.displayCanvasContext, true);
            if (this.props.useBuffer) {
                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d');
                this._buffer = this.createCanvasInstance(canvas, context, false);
            }
        }
        /**
         * Creates a canvas instance.
         * @param canvas The canvas element.
         * @param context The canvas 2D rendering context.
         * @param isDisplayed Whether the canvas is displayed.
         * @returns a canvas instance.
         */
        createCanvasInstance(canvas, context, isDisplayed) {
            return new MapCanvasLayerCanvasInstanceClass(canvas, context, isDisplayed);
        }
        /**
         * Updates the canvas element's size.
         */
        updateCanvasSize() {
            const displayCanvas = this.display.canvas;
            displayCanvas.width = this.width;
            displayCanvas.height = this.height;
            displayCanvas.style.width = `${this.width}px`;
            displayCanvas.style.height = `${this.height}px`;
            if (this._buffer) {
                const bufferCanvas = this._buffer.canvas;
                bufferCanvas.width = this.width;
                bufferCanvas.height = this.height;
            }
        }
        /**
         * Updates the visibility of the display canvas.
         */
        updateCanvasVisibility() {
            this.display.canvas.style.display = this.isVisible() ? 'block' : 'none';
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("canvas", { ref: this.displayCanvasRef, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '', width: '0', height: '0', style: 'position: absolute;' }));
        }
    }

    /**
     * A canvas map layer whose size and position is synced with the map projection window.
     */
    class MapSyncedCanvasLayer extends MapCanvasLayer {
        // eslint-disable-next-line jsdoc/require-jsdoc
        onAttached() {
            super.onAttached();
            this.updateFromProjectedSize(this.props.mapProjection.getProjectedSize());
        }
        /**
         * Updates this layer according to the current size of the projected map window.
         * @param projectedSize The size of the projected map window.
         */
        updateFromProjectedSize(projectedSize) {
            this.setWidth(projectedSize[0]);
            this.setHeight(projectedSize[1]);
            const displayCanvas = this.display.canvas;
            displayCanvas.style.left = '0px';
            displayCanvas.style.top = '0px';
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        onMapProjectionChanged(mapProjection, changeFlags) {
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.updateFromProjectedSize(mapProjection.getProjectedSize());
            }
        }
    }

    /* eslint-disable @typescript-eslint/no-non-null-assertion */
    /**
     * A map layer containing a single canvas synced to the map's projected size that can be shared amongst multiple
     * sublayers for rendering.
     *
     * All of the layer's children are rendered on top of the shared canvas element. All children that extend
     * {@link MapSharedCanvasSubLayer} are treated as sublayers and can render to the shared canvas element.
     */
    class MapSharedCanvasLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.sublayers = [];
            this.isInit = false;
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            if (!this.isInit) {
                return;
            }
            this.canvasLayerRef.instance.setVisible(isVisible);
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].setVisible(isVisible);
            }
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            this.thisNode = thisNode;
            // Enumerate sublayers
            exports.FSComponent.visitNodes(thisNode, node => {
                if (node !== thisNode && node.instance instanceof DisplayComponent) {
                    if (node.instance instanceof MapSharedCanvasSubLayer) {
                        this.sublayers.push(node.instance);
                    }
                    return true;
                }
                return false;
            });
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.canvasLayerRef.instance.onAttached();
            this.sharedCanvasInstance = new MapSharedCanvasInstanceClass(this.canvasLayerRef.instance.display);
            this.isInit = true;
            if (!this.isVisible()) {
                this.onVisibilityChanged(false);
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].attach(this.props.mapProjection, this.sharedCanvasInstance);
            }
        }
        /** @inheritdoc */
        onWake() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onWake();
            }
        }
        /** @inheritdoc */
        onSleep() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onSleep();
            }
        }
        /** @inheritdoc */
        onMapProjectionChanged(projection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(projection, changeFlags);
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.sharedCanvasInstance.invalidate();
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onMapProjectionChanged(projection, changeFlags);
            }
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            let invalidate = false;
            for (let i = 0; !invalidate && i < this.sublayers.length; i++) {
                invalidate = this.sublayers[i].shouldInvalidate(time, elapsed);
            }
            if (invalidate) {
                this.sharedCanvasInstance.invalidate();
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onUpdated(time, elapsed);
            }
            this.sharedCanvasInstance.revalidate();
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null,
                exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection }),
                this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            this.thisNode && exports.FSComponent.shallowDestroy(this.thisNode);
            super.destroy();
        }
    }
    /**
     * A sublayer of {@link MapSharedCanvasLayer}.
     */
    class MapSharedCanvasSubLayer extends DisplayComponent {
        constructor() {
            super(...arguments);
            this._isAttached = false;
            this._isVisible = true;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's map projection.
         * @throws Error if this sublayer is not attached.
         */
        get projection() {
            if (this._projection) {
                return this._projection;
            }
            throw new Error('MapSharedCanvasSubLayer: attempted to access projection before sublayer was attached');
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's shared canvas instance.
         * @throws Error if this sublayer is not attached.
         */
        get display() {
            if (this._display) {
                return this._display;
            }
            throw new Error('MapSharedCanvasSubLayer: attempted to access display canvas before sublayer was attached');
        }
        /**
         * Checks whether this sublayer is attached to a parent layer.
         * @returns Whether this sublayer is attached to a parent layer.
         */
        isAttached() {
            return this._isAttached;
        }
        /**
         * Checks whether this sublayer is visible.
         * @returns Whether this sublayer is visible.
         */
        isVisible() {
            return this._isVisible;
        }
        /**
         * Attaches this sublayer to a parent layer.
         * @param projection The map projection used by this sublayer.
         * @param display The canvas instance shared by this sublayer.
         */
        attach(projection, display) {
            this._projection = projection;
            this._display = display;
            this._isAttached = true;
            if (!this._isVisible) {
                this.onVisibilityChanged(this._isVisible);
            }
            this.onAttached();
        }
        /**
         * Sets this sublayer's visibility.
         * @param val Whether this sublayer should be visible.
         */
        setVisible(val) {
            if (this._isVisible === val) {
                return;
            }
            this._isVisible = val;
            if (this._isAttached) {
                this.onVisibilityChanged(val);
            }
        }
        /**
         * This method is called when this layer's visibility changes.
         * @param isVisible Whether the layer is now visible.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            // noop
        }
        /**
         * This method is called when this sublayer is attached to its parent layer.
         */
        onAttached() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is awakened.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when this sublayer's map projection changes.
         * @param projection This sublayer's map projection.
         * @param changeFlags The types of changes made to the projection.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onMapProjectionChanged(projection, changeFlags) {
            // noop
        }
        /**
         * This method is called at the beginning of every update cycle to check whether this sublayer's shared canvas
         * instance should be invalidated. If the canvas is already invalidated, then this method will not be called.
         * @param time The current time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         * @returns Whether this sublayer's shared canvas instance should be invalidated.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        shouldInvalidate(time, elapsed) {
            return false;
        }
        /**
         * This method is called once every update cycle after this sublayer's shared canvas instance has had a chance to be
         * invalidated.
         * @param time The current time as a UNIX timestamp in milliseconds.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            // noop
        }
        /** @inheritdoc */
        render() {
            return null;
        }
    }
    /**
     * An implementation of {@link MapSharedCanvasInstance} which is backed by a
     * {@link MapCanvasLayerCanvasInstance}.
     */
    class MapSharedCanvasInstanceClass {
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether this canvas has been invalidated. */
        get isInvalidated() {
            return this._isInvalidated;
        }
        /**
         * Creates a new instance of MapSharedCanvasInstanceClass.
         * @param instance This instance's backing canvas instance.
         */
        constructor(instance) {
            this.instance = instance;
            /** @inheritdoc */
            this.canvas = this.instance.canvas;
            /** @inheritdoc */
            this.context = this.instance.context;
            this._isInvalidated = false;
        }
        /**
         * Invalidates and clears this canvas.
         */
        invalidate() {
            this._isInvalidated = true;
            this.instance.clear();
        }
        /** @inheritdoc */
        revalidate() {
            this._isInvalidated = false;
        }
    }

    /**
     * A generic implementation of {@link MapSharedCanvasSubLayer} that delegates behavior to props.
     */
    class GenericMapSharedCanvasSubLayer extends MapSharedCanvasSubLayer {
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            this.props.onVisibilityChanged && this.props.onVisibilityChanged(isVisible);
        }
        /** @inheritdoc */
        onAttached() {
            this.props.onAttached && this.props.onAttached(this.projection, this.display);
        }
        /** @inheritdoc */
        onWake() {
            this.props.onWake && this.props.onWake(this.projection, this.display);
        }
        /** @inheritdoc */
        onSleep() {
            this.props.onSleep && this.props.onSleep(this.projection, this.display);
        }
        /** @inheritdoc */
        shouldInvalidate(time, elapsed) {
            return this.props.shouldInvalidate ? this.props.shouldInvalidate(this.projection, this.display, time, elapsed) : false;
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            this.props.onUpdated && this.props.onUpdated(this.projection, this.display, time, elapsed);
        }
        /** @inheritdoc */
        destroy() {
            this.props.onDestroyed && this.props.onDestroyed();
            super.destroy();
        }
    }

    /** Utilities for AIRAC cycles. */
    class AiracUtils {
        /**
         * Parses the MSFS facility database effective dates into an AIRAC cycle.
         * @param facilitiesDateRange The MSFS facilities date range from the game var `FLIGHT NAVDATA DATE RANGE`.
         * @param out An optional object to write the result to. If not specified a new object will be used.
         * @returns The AIRAC cycle information for the MSFS facility database, or undefined if an error occurs.
         */
        static parseFacilitiesCycle(facilitiesDateRange, out) {
            const match = facilitiesDateRange.match(AiracUtils.MSFS_DATE_RANGE_REGEX);
            if (match === null) {
                console.warn('AiracUtils: Failed to parse facilitiesDateRange', facilitiesDateRange);
                return undefined;
            }
            const [, effMonth, effDay, expMonth, expDay, expYear] = match;
            const effDate = new Date(`${effMonth}-${effDay}-${expYear} UTC`);
            const expDate = new Date(`${expMonth}-${expDay}-${expYear} UTC`);
            // We need to work around a bug where the sim gives the year of the expiration date rather than the effective date.
            if (effDate.getTime() > expDate.getTime()) {
                effDate.setUTCFullYear(effDate.getUTCFullYear() - 1);
            }
            const effectiveTimestamp = effDate.getTime();
            const result = out !== undefined ? out : {};
            return AiracUtils.fillCycleFromEffectiveTimestamp(effectiveTimestamp, result);
        }
        /**
         * Gets a cycle offset from another cycle in increments of 28-days (the length of an AIRAC cycle).
         * @param baseCycle The base cycle to offset from.
         * @param offset The offset in number of cycles (i.e. 28-day increments).
         * @param out An optional object to write the result to. If not specified a new object will be used.
         * @returns The AIRAC cycle information.
         */
        static getOffsetCycle(baseCycle, offset, out) {
            const effectiveTimestamp = baseCycle.effectiveTimestamp + offset * AiracUtils.CYCLE_DURATION;
            const result = out !== undefined ? out : {};
            return AiracUtils.fillCycleFromEffectiveTimestamp(effectiveTimestamp, result);
        }
        /**
         * Gets the AIRAC cycle number in the effective year, given the effective date.
         * @param effectiveTimestamp The effective timestamp, in milliseconds since the UNIX epoch, to determine the cycle number for.
         * @returns The cycle number.
         */
        static getCycleNumber(effectiveTimestamp) {
            AiracUtils.dateCache.setTime(effectiveTimestamp);
            const january1 = Date.UTC(AiracUtils.dateCache.getUTCFullYear(), 0, 1);
            const january1Delta = effectiveTimestamp - january1;
            return Math.trunc(january1Delta / AiracUtils.CYCLE_DURATION) + 1;
        }
        /**
         * Gets the current cycle for a given date.
         * @param date The date to determine the current cycle for.
         * @param out An optional object to write the result to. If not specified a new object will be used.
         * @returns The AIRAC cycle information.
         */
        static getCurrentCycle(date, out) {
            const datumDelta = date.getTime() - AiracUtils.DATUM_CYCLE_TIMESTAMP;
            const offset = Math.floor(datumDelta / AiracUtils.CYCLE_DURATION);
            const effectiveTimestamp = AiracUtils.DATUM_CYCLE_TIMESTAMP + offset * AiracUtils.CYCLE_DURATION;
            const result = out !== undefined ? out : {};
            return AiracUtils.fillCycleFromEffectiveTimestamp(effectiveTimestamp, result);
        }
        /**
         * Fills an {@link AiracCycle} object given an effective timestamp.
         * @param effectiveTimestamp The time this cycle becomes effective in milliseonds since the UNIX epoch.
         * @param out The output object.
         * @returns The object passed as `out` with all the data filled.
         */
        static fillCycleFromEffectiveTimestamp(effectiveTimestamp, out) {
            AiracUtils.dateCache.setTime(effectiveTimestamp);
            out.effectiveTimestamp = effectiveTimestamp;
            out.expirationTimestamp = effectiveTimestamp + AiracUtils.CYCLE_DURATION;
            out.cycle = AiracUtils.getCycleNumber(effectiveTimestamp);
            out.cycleString = out.cycle.toString().padStart(2, '0');
            out.ident = `${(AiracUtils.dateCache.getUTCFullYear() % 100).toString().padStart(2, '0')}${out.cycleString}`;
            return out;
        }
    }
    AiracUtils.dateCache = new Date();
    /** Duration of an AIRAC cycle (28 days) in milliseconds. */
    AiracUtils.CYCLE_DURATION = 86400000 * 28;
    AiracUtils.MSFS_DATE_RANGE_REGEX = /([A-Z]{3})(\d\d?)([A-Z]{3})(\d\d?)\/(\d\d)/;
    // Reference cycle to allow us to calculate the current cycle for an arbitrary date (2401, effective 25 JAN 2024).
    AiracUtils.DATUM_CYCLE_TIMESTAMP = Date.UTC(2024, 0, 25);

    /**
     * Utility functions for working with airport data.
     */
    class AirportUtils {
        /**
         * Attempts to get the region code of an airport.
         * @param facility The facility record for the airport.
         * @returns The region code of an airport, or `undefined` if one could not be found.
         */
        static tryGetRegionCode(facility) {
            // Airports don't have region codes in their ICAO strings, so we will try a series of increasingly ugly hacks to
            // deduce the region code
            // First, we will look for any non-circling approach and see if we can find a runway fix and grab its region code,
            // which should always be the same code as the airport
            for (let i = 0; i < facility.approaches.length; i++) {
                const approach = facility.approaches[i];
                if (approach.runway.length === 0 || approach.finalLegs.length === 0) {
                    continue;
                }
                const fixIcao = approach.finalLegs[approach.finalLegs.length - 1].fixIcao;
                if (ICAO.isFacility(fixIcao, exports.FacilityType.RWY)) {
                    const region = ICAO.getRegionCode(fixIcao);
                    if (AirportUtils.REGION_CODES.has(region)) {
                        return region;
                    }
                }
            }
            // Next, we will grab region codes from final approach fixes. If they all match, then it's a good bet the airport
            // region code is the same.
            if (facility.approaches.length > 1) {
                let region = undefined;
                let regionCount = 0;
                for (let i = 0; i < facility.approaches.length; i++) {
                    const approach = facility.approaches[i];
                    for (let j = 0; j < approach.finalLegs.length; j++) {
                        const leg = approach.finalLegs[j];
                        if (leg.fixTypeFlags === exports.FixTypeFlags.FAF && ICAO.isFacility(leg.fixIcao)) {
                            const fafRegion = ICAO.getRegionCode(leg.fixIcao);
                            if (AirportUtils.REGION_CODES.has(fafRegion)) {
                                region !== null && region !== void 0 ? region : (region = fafRegion);
                                if (region !== fafRegion) {
                                    region = undefined;
                                }
                                regionCount++;
                                break;
                            }
                        }
                    }
                    if (region === undefined && regionCount > 0) {
                        break;
                    }
                }
                if (region !== undefined && regionCount > 1) {
                    return region;
                }
            }
            // Next, we will grab the first two letters of the airport ident if the ident is exactly four letters and does not
            // contain any numerals.
            const ident = ICAO.getIdent(facility.icao);
            if (ident.length === 4 && ident.search(AirportUtils.NUMERAL_REGEX) < 0) {
                const region = ident.substring(0, 2);
                if (AirportUtils.REGION_CODES.has(region)) {
                    return region;
                }
            }
            // Finally, we will search every procedure (excluding enroute transitions) at the airport for terminal intersections.
            // The region codes of these intersections should be the same as that of the airport.
            // Departures
            for (let i = 0; i < facility.departures.length; i++) {
                const departure = facility.departures[i];
                for (let j = 0; j < departure.commonLegs.length; j++) {
                    const leg = departure.commonLegs[j];
                    if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                        const region = ICAO.getRegionCode(leg.fixIcao);
                        if (AirportUtils.REGION_CODES.has(region)) {
                            return region;
                        }
                    }
                }
                for (let j = 0; j < departure.runwayTransitions.length; j++) {
                    const transition = departure.runwayTransitions[j];
                    for (let k = 0; k < transition.legs.length; k++) {
                        const leg = transition.legs[k];
                        if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                            const region = ICAO.getRegionCode(leg.fixIcao);
                            if (AirportUtils.REGION_CODES.has(region)) {
                                return region;
                            }
                        }
                    }
                }
            }
            // Arrivals
            for (let i = 0; i < facility.arrivals.length; i++) {
                const arrival = facility.arrivals[i];
                for (let j = 0; j < arrival.commonLegs.length; j++) {
                    const leg = arrival.commonLegs[j];
                    if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                        const region = ICAO.getRegionCode(leg.fixIcao);
                        if (AirportUtils.REGION_CODES.has(region)) {
                            return region;
                        }
                    }
                }
                for (let j = 0; j < arrival.runwayTransitions.length; j++) {
                    const transition = arrival.runwayTransitions[j];
                    for (let k = 0; k < transition.legs.length; k++) {
                        const leg = transition.legs[k];
                        if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                            const region = ICAO.getRegionCode(leg.fixIcao);
                            if (AirportUtils.REGION_CODES.has(region)) {
                                return region;
                            }
                        }
                    }
                }
            }
            // Approaches
            for (let i = 0; i < facility.approaches.length; i++) {
                const approach = facility.approaches[i];
                for (let j = 0; j < approach.finalLegs.length; j++) {
                    const leg = approach.finalLegs[j];
                    if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                        const region = ICAO.getRegionCode(leg.fixIcao);
                        if (AirportUtils.REGION_CODES.has(region)) {
                            return region;
                        }
                    }
                }
                for (let j = 0; j < approach.transitions.length; j++) {
                    const transition = approach.transitions[j];
                    for (let k = 0; k < transition.legs.length; k++) {
                        const leg = transition.legs[k];
                        if (ICAO.isFacility(leg.fixIcao) && ICAO.getAssociatedAirportIdent(leg.fixIcao) === ident) {
                            const region = ICAO.getRegionCode(leg.fixIcao);
                            if (AirportUtils.REGION_CODES.has(region)) {
                                return region;
                            }
                        }
                    }
                }
            }
            return undefined;
        }
        /**
         * Gets the elevation of an airport, in meters. The elevation is estimated as the average elevation of the airport's
         * runways. If the airport has no runways, an elevation cannot be estimated and `undefined` is returned instead.
         * @param facility The facility record for the airport.
         * @returns The elevation of the specified airport, in meters, or `undefined` if the elevation could not be
         * determined.
         */
        static getElevation(facility) {
            if (facility.runways.length === 0) {
                return undefined;
            }
            return facility.runways.reduce((sum, runway) => sum + runway.elevation, 0) / facility.runways.length;
        }
        /**
         * Gets the longest runway of an airport.
         * @param facility The facility record for the airport.
         * @returns The longest runway as an AirportRunway, or null.
         */
        static getLongestRunway(facility) {
            let longestRunway = null;
            for (const runway of facility.runways) {
                if (longestRunway === null || runway.length > longestRunway.length) {
                    longestRunway = runway;
                }
            }
            return longestRunway;
        }
        /**
         * Get a list of runways at an airport matching specific criteria.
         * @param facility The facility record for the airport.
         * @param minLength The minimum length of the runway, in feet.
         * @param surfaceTypes An optional bitfield of RunwaySurfaceCategory values to allow.
         * @returns A list of matching runways.
         */
        static getFilteredRunways(facility, minLength, surfaceTypes) {
            minLength = UnitType.METER.convertFrom(minLength, UnitType.FOOT);
            const result = [];
            for (const runway of facility.runways) {
                if (runway.length >= minLength) {
                    if (surfaceTypes === undefined ||
                        BitFlags.isAny(RunwayUtils.getSurfaceCategory(runway), surfaceTypes)) {
                        result.push(runway);
                    }
                }
            }
            return result;
        }
        /**
         * Checks to see whether an airport has a runway matching specific criteria.   This is a
         * lighter version of getFilteredRunways that doesn't do any extra assignments.
         * @param facility The facility record for the airport.
         * @param minLength The minimum length of the runway, in feet.
         * @param surfaceTypes An optional bitfield of RunwaySurfaceCategory values to allow.
         * @returns A boolean if a matching runway exists.
         */
        static hasMatchingRunway(facility, minLength, surfaceTypes) {
            minLength = UnitType.METER.convertFrom(minLength, UnitType.FOOT);
            for (const runway of facility.runways) {
                if (runway.length >= minLength) {
                    if (surfaceTypes === undefined ||
                        BitFlags.isAny(RunwayUtils.getSurfaceCategory(runway), surfaceTypes)) {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * Finds the index of a named departure or arrival procedure in an airport facility procedure array, the index of a
         * named enroute transition in the procedure's enroute transition array, and the index of a runway transition in the
         * procedure's runway transition array for a given runway.
         * @param procedureArray The procedure array in which to search for the procedure.
         * @param procedureName The name of the procedure to find.
         * @param transitionName The name of the procedure enroute transition to find.
         * @param runway The identifier for the runway associated with the procedure runway transition to find.
         * @returns The indexes of the specified departure or arrival procedure in the specified procedure array, of the
         * specified enroute transition in the procedure's enroute transition array, and of the specified runway transition
         * in the procedure's runway transition array.
         */
        static findDepartureArrivalIndexesFromName(procedureArray, procedureName, transitionName, runway) {
            const result = {
                procedureIndex: -1,
                enrouteTransitionIndex: -1,
                runwayTransitionIndex: -1
            };
            if (procedureName !== '') {
                for (let procedureIndex = 0; procedureIndex < procedureArray.length; procedureIndex++) {
                    const procedure = procedureArray[procedureIndex];
                    if (procedure.name === procedureName) {
                        result.procedureIndex = procedureIndex;
                        if (transitionName !== '') {
                            for (let enrouteTransitionIndex = 0; enrouteTransitionIndex < procedure.enRouteTransitions.length; enrouteTransitionIndex++) {
                                const transition = procedure.enRouteTransitions[enrouteTransitionIndex];
                                if (transition.name === transitionName) {
                                    result.enrouteTransitionIndex = enrouteTransitionIndex;
                                    break;
                                }
                            }
                        }
                        if (runway.number !== '') {
                            for (let runwayTransitionIndex = 0; runwayTransitionIndex < procedure.runwayTransitions.length; runwayTransitionIndex++) {
                                const transition = procedure.runwayTransitions[runwayTransitionIndex];
                                if (RunwayUtils.getNumberString(transition.runwayNumber) === runway.number
                                    && RunwayUtils.getDesignatorLetter(transition.runwayDesignation) === runway.designator) {
                                    result.runwayTransitionIndex = runwayTransitionIndex;
                                    break;
                                }
                            }
                        }
                        break;
                    }
                }
            }
            return result;
        }
        /**
         * Finds the index of an approach in an airport facility described by an approach identifier and the index of a
         * named transition in the approach's transition array.
         * @param airport The airport facility in which to search for the approach.
         * @param identifier The identifier for the approach to find.
         * @param transitionName The name of the approach transition to find.
         * @returns The indexes of the specified approach in the airport facility's approach array and of the specified
         * transition in the approach's transition array.
         */
        static findApproachIndexesFromIdentifier(airport, identifier, transitionName) {
            const result = {
                approachIndex: -1,
                transitionIndex: -1
            };
            if (airport.approaches.length === 0) {
                return result;
            }
            const approachType = ApproachUtils.nameToType(identifier.type);
            if (approachType === ApproachType.APPROACH_TYPE_UNKNOWN) {
                return result;
            }
            for (let approachIndex = 0; approachIndex < airport.approaches.length; approachIndex++) {
                const approach = airport.approaches[approachIndex];
                if (approach.approachType === approachType
                    && RunwayUtils.getNumberString(approach.runwayNumber) === identifier.runway.number
                    && RunwayUtils.getDesignatorLetter(approach.runwayDesignator) === identifier.runway.designator
                    && approach.approachSuffix === identifier.suffix) {
                    result.approachIndex = approachIndex;
                    if (transitionName !== '') {
                        for (let transitionIndex = 0; transitionIndex < approach.transitions.length; transitionIndex++) {
                            const transition = approach.transitions[transitionIndex];
                            if (transition.name === transitionName) {
                                result.transitionIndex = transitionIndex;
                                break;
                            }
                        }
                    }
                    break;
                }
            }
            return result;
        }
    }
    AirportUtils.REGION_CODES = new Set([
        'AG', 'AN', 'AY', 'BG', 'BI', 'BK', 'CY', 'DA', 'DB', 'DF', 'DG', 'DI', 'DN', 'DR', 'DT', 'DX', 'EB', 'ED', 'EE',
        'EF', 'EG', 'EH', 'EI', 'EK', 'EL', 'EN', 'EP', 'ES', 'ET', 'EV', 'EY', 'FA', 'FB', 'FC', 'FD', 'FE', 'FG', 'FH',
        'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FS', 'FT', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC',
        'GE', 'GF', 'GG', 'GL', 'GM', 'GO', 'GQ', 'GS', 'GU', 'GV', 'HA', 'HB', 'HD', 'HE', 'HH', 'HK', 'HL', 'HR', 'HS',
        'HT', 'HU', 'K1', 'K2', 'K3', 'K4', 'K5', 'K6', 'K7', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ',
        'LK', 'LL', 'LM', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MB', 'MD', 'MG', 'MH',
        'MK', 'MM', 'MN', 'MP', 'MR', 'MS', 'MT', 'MU', 'MW', 'MY', 'MZ', 'NC', 'NF', 'NG', 'NI', 'NL', 'NS', 'NT', 'NV',
        'NW', 'NZ', 'OA', 'OB', 'OE', 'OI', 'OJ', 'OK', 'OL', 'OM', 'OO', 'OP', 'OR', 'OS', 'OT', 'OY', 'PA', 'PG', 'PH',
        'PJ', 'PK', 'PL', 'PM', 'PO', 'PP', 'PT', 'PW', 'RC', 'RJ', 'RK', 'RO', 'RP', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG',
        'SI', 'SJ', 'SK', 'SL', 'SM', 'SO', 'SP', 'SS', 'SU', 'SV', 'SW', 'SY', 'TA', 'TB', 'TD', 'TF', 'TG', 'TI', 'TJ',
        'TK', 'TL', 'TN', 'TQ', 'TT', 'TU', 'TV', 'TX', 'UA', 'UB', 'UC', 'UD', 'UE', 'UG', 'UH', 'UI', 'UK', 'UL', 'UM',
        'UN', 'UO', 'UR', 'US', 'UT', 'UU', 'UW', 'VA', 'VC', 'VD', 'VE', 'VG', 'VH', 'VI', 'VL', 'VM', 'VN', 'VO', 'VR',
        'VT', 'VV', 'VY', 'WA', 'WB', 'WI', 'WM', 'WR', 'WS', 'YB', 'YM', 'ZB', 'ZG', 'ZH', 'ZK', 'ZL', 'ZM', 'ZP', 'ZS',
        'ZU', 'ZW', 'ZY'
    ]);
    AirportUtils.NUMERAL_REGEX = /\d/;

    /**
     * Types of airspaces.
     */
    exports.AirspaceType = void 0;
    (function (AirspaceType) {
        AirspaceType[AirspaceType["None"] = 0] = "None";
        AirspaceType[AirspaceType["Center"] = 1] = "Center";
        AirspaceType[AirspaceType["ClassA"] = 2] = "ClassA";
        AirspaceType[AirspaceType["ClassB"] = 3] = "ClassB";
        AirspaceType[AirspaceType["ClassC"] = 4] = "ClassC";
        AirspaceType[AirspaceType["ClassD"] = 5] = "ClassD";
        AirspaceType[AirspaceType["ClassE"] = 6] = "ClassE";
        AirspaceType[AirspaceType["ClassF"] = 7] = "ClassF";
        AirspaceType[AirspaceType["ClassG"] = 8] = "ClassG";
        AirspaceType[AirspaceType["Tower"] = 9] = "Tower";
        AirspaceType[AirspaceType["Clearance"] = 10] = "Clearance";
        AirspaceType[AirspaceType["Ground"] = 11] = "Ground";
        AirspaceType[AirspaceType["Departure"] = 12] = "Departure";
        AirspaceType[AirspaceType["Approach"] = 13] = "Approach";
        AirspaceType[AirspaceType["MOA"] = 14] = "MOA";
        AirspaceType[AirspaceType["Restricted"] = 15] = "Restricted";
        AirspaceType[AirspaceType["Prohibited"] = 16] = "Prohibited";
        AirspaceType[AirspaceType["Warning"] = 17] = "Warning";
        AirspaceType[AirspaceType["Alert"] = 18] = "Alert";
        AirspaceType[AirspaceType["Danger"] = 19] = "Danger";
        AirspaceType[AirspaceType["Nationalpark"] = 20] = "Nationalpark";
        AirspaceType[AirspaceType["ModeC"] = 21] = "ModeC";
        AirspaceType[AirspaceType["Radar"] = 22] = "Radar";
        AirspaceType[AirspaceType["Training"] = 23] = "Training";
        AirspaceType[AirspaceType["Max"] = 24] = "Max";
    })(exports.AirspaceType || (exports.AirspaceType = {}));

    /**
     *
     */
    class CoherentAirspace {
        /**
         * Constructor.
         * @param def The airspace definition to use for the new airspace.
         * @param uid A unique string ID for the new airspace.
         */
        constructor(def, uid) {
            /** @inheritdoc */
            this.name = '';
            this._segments = [];
            this.type = def.type;
            this.uid = uid;
            const segments = def.segments;
            const len = segments.length;
            for (let i = 0; i < len; i++) {
                const point = segments[i];
                this._segments[i] = new GeoPoint(point.lat, point.long);
            }
        }
        /** @inheritdoc */
        get segments() {
            return this._segments;
        }
        /** @inheritdoc */
        equals(other) {
            if (other instanceof CoherentAirspace) {
                return this.uid === other.uid;
            }
            return this.type === other.type
                && this.segments.length === other.segments.length
                && this.segments.every((point, index) => point.equals(other.segments[index]));
        }
    }
    /**
     * A searcher for airspaces.
     */
    class AirspaceSearcher {
        /**
         * Constructor.
         * @param cacheSize The maximum size of the Airspace cache.
         */
        constructor(cacheSize = AirspaceSearcher.DEFAULT_CACHE_SIZE) {
            this.cacheSize = cacheSize;
            this.cache = new Map();
            this._isBusy = false;
            this.queue = [];
        }
        /**
         * Checks whether this searcher is currently busy with a search.
         * @returns whether this searcher is currently busy with a search.
         */
        isBusy() {
            return this._isBusy;
        }
        /**
         * Searches for airspaces around a geographic point. If the searcher is not busy, the search will execute
         * immediately. If the search is busy, the search will be queued. Queued searches will be executed one at a time in
         * FIFO order as searches are finished.
         * @param center The center of the search area.
         * @returns a Promise which is fulfilled with an array of airspaces when the search finishes.
         */
        search(center) {
            return new Promise(resolve => {
                if (this._isBusy || this.queue.length > 0) {
                    this.enqueueSearch(center, resolve);
                }
                else {
                    this.doSearch(center, resolve);
                }
            });
        }
        /**
         * Enqueues a search operation.
         * @param center The center of the search area.
         * @param resolve The Promise resolve function to call with the search results.
         */
        enqueueSearch(center, resolve) {
            this.queue.push(this.doSearch.bind(this, center, resolve));
        }
        /**
         * Executes the next search operation in the queue, if one exists.
         */
        processQueue() {
            const next = this.queue.shift();
            if (next) {
                next();
            }
        }
        /**
         * Executes an airspace search.
         * @param center The center of the search area.
         * @param resolve The Promise resolve function to call with the search results.
         */
        async doSearch(center, resolve) {
            this._isBusy = true;
            try {
                const coherentDefs = await Promise.race([
                    this.executeCoherentSearch(center),
                    new Promise((timeoutResolve, reject) => setTimeout(() => reject('Airspace search timed out.'), AirspaceSearcher.SEARCH_TIMEOUT))
                ]);
                const airspaces = this.processCoherentDefs(coherentDefs);
                resolve(airspaces);
            }
            catch (e) {
                // console.log(e);
                resolve([]);
            }
            this._isBusy = false;
            this.processQueue();
        }
        /**
         * Executes a Coherent airspace search.
         * @param center The center of the search area.
         * @returns a Promise which is fulfilled with an array of Coherent airspace definitions when the search finishes.
         */
        async executeCoherentSearch(center) {
            await Coherent.call('SET_LOAD_LATLON', center.lat, center.lon);
            return await Coherent.call('GET_NEAREST_AIRSPACES');
        }
        /**
         * Processes an array of Coherent airspace definitions into an array of Airspaces.
         * @param defs An array fo Coherent airspace definitions.
         * @returns an array of Airspaces corresponding to the supplied definitions.
         */
        processCoherentDefs(defs) {
            const result = [];
            const len = defs.length;
            for (let i = 0; i < len; i++) {
                const def = defs[i];
                if (def.type === exports.AirspaceType.None) {
                    continue;
                }
                const uid = AirspaceSearcher.generateUID(def);
                let airspace = this.cache.get(uid);
                if (!airspace) {
                    airspace = new CoherentAirspace(def, uid);
                    this.cacheAirspace(airspace);
                }
                result.push(airspace);
            }
            return result;
        }
        /**
         * Adds an airspace to the cache. If the cache size exceeds the maximum after the operation, airspaces will be
         * removed from the cache in FIFO order to maintain the maximum cache size.
         * @param airspace The airspace to cache.
         */
        cacheAirspace(airspace) {
            this.cache.set(airspace.uid, airspace);
            if (this.cache.size > this.cacheSize) {
                this.cache.delete(this.cache.keys().next().value);
            }
        }
        /**
         * Generates a unique string ID for a Coherent airspace definition.
         * @param def The airspace definition.
         * @returns a unique string ID.
         */
        static generateUID(def) {
            const segments = def.segments;
            let uid = `${def.type}[${segments.length}]:`;
            // skip last vertex since it is always a repeat of the first; cap length to 10 to avoid creating super long strings
            const len = Math.min(segments.length - 1, 10);
            for (let i = 0; i < len; i++) {
                const point = segments[i];
                uid += `(${point.lat},${point.long})`;
            }
            // if vertices were skipped, grab the last (unique) vertex to decrease chance of uid collision.
            if (len < segments.length - 1) {
                const point = segments[segments.length - 2];
                uid += `(${point.lat},${point.long})`;
            }
            return uid;
        }
    }
    /** The amount of time to wait for a search to finish before it times out, in milliseconds. */
    AirspaceSearcher.SEARCH_TIMEOUT = 5000;
    AirspaceSearcher.DEFAULT_CACHE_SIZE = 1000;

    /**
     * A viewlistener that gets autopilot mode information.
     */
    exports.MSFSAPStates = void 0;
    (function (MSFSAPStates) {
        MSFSAPStates[MSFSAPStates["LogicOn"] = 1] = "LogicOn";
        MSFSAPStates[MSFSAPStates["APOn"] = 2] = "APOn";
        MSFSAPStates[MSFSAPStates["FDOn"] = 4] = "FDOn";
        MSFSAPStates[MSFSAPStates["FLC"] = 8] = "FLC";
        MSFSAPStates[MSFSAPStates["Alt"] = 16] = "Alt";
        MSFSAPStates[MSFSAPStates["AltArm"] = 32] = "AltArm";
        MSFSAPStates[MSFSAPStates["GS"] = 64] = "GS";
        MSFSAPStates[MSFSAPStates["GSArm"] = 128] = "GSArm";
        MSFSAPStates[MSFSAPStates["Pitch"] = 256] = "Pitch";
        MSFSAPStates[MSFSAPStates["VS"] = 512] = "VS";
        MSFSAPStates[MSFSAPStates["Heading"] = 1024] = "Heading";
        MSFSAPStates[MSFSAPStates["Nav"] = 2048] = "Nav";
        MSFSAPStates[MSFSAPStates["NavArm"] = 4096] = "NavArm";
        MSFSAPStates[MSFSAPStates["WingLevel"] = 8192] = "WingLevel";
        MSFSAPStates[MSFSAPStates["Attitude"] = 16384] = "Attitude";
        MSFSAPStates[MSFSAPStates["ThrottleSpd"] = 32768] = "ThrottleSpd";
        MSFSAPStates[MSFSAPStates["ThrottleMach"] = 65536] = "ThrottleMach";
        MSFSAPStates[MSFSAPStates["ATArm"] = 131072] = "ATArm";
        MSFSAPStates[MSFSAPStates["YD"] = 262144] = "YD";
        MSFSAPStates[MSFSAPStates["EngineRPM"] = 524288] = "EngineRPM";
        MSFSAPStates[MSFSAPStates["TOGAPower"] = 1048576] = "TOGAPower";
        MSFSAPStates[MSFSAPStates["Autoland"] = 2097152] = "Autoland";
        MSFSAPStates[MSFSAPStates["TOGAPitch"] = 4194304] = "TOGAPitch";
        MSFSAPStates[MSFSAPStates["Bank"] = 8388608] = "Bank";
        MSFSAPStates[MSFSAPStates["FBW"] = 16777216] = "FBW";
        MSFSAPStates[MSFSAPStates["AvionicsManaged"] = 33554432] = "AvionicsManaged";
        MSFSAPStates[MSFSAPStates["None"] = -2147483648] = "None";
    })(exports.MSFSAPStates || (exports.MSFSAPStates = {}));

    /**
     * A collection of unique string waypoint type keys.
     */
    exports.WaypointTypes = void 0;
    (function (WaypointTypes) {
        WaypointTypes["Custom"] = "Custom";
        WaypointTypes["Airport"] = "Airport";
        WaypointTypes["NDB"] = "NDB";
        WaypointTypes["VOR"] = "VOR";
        WaypointTypes["Intersection"] = "Intersection";
        WaypointTypes["Runway"] = "Runway";
        WaypointTypes["User"] = "User";
        WaypointTypes["Visual"] = "Visual";
        WaypointTypes["FlightPlan"] = "FlightPlan";
        WaypointTypes["VNAV"] = "VNAV";
    })(exports.WaypointTypes || (exports.WaypointTypes = {}));
    /**
     * An abstract implementation of Waypoint.
     */
    class AbstractWaypoint {
        // eslint-disable-next-line jsdoc/require-jsdoc
        equals(other) {
            return this.uid === other.uid;
        }
    }
    /**
     * A waypoint with custom defined lat/lon coordinates.
     */
    class CustomWaypoint extends AbstractWaypoint {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(arg1, arg2, arg3) {
            super();
            let location;
            let uid;
            if (typeof arg1 === 'number') {
                location = GeoPointSubject.create(new GeoPoint(arg1, arg2));
                uid = `${arg3}[${location.get().lat},${location.get().lon}]`;
            }
            else {
                location = arg1;
                uid = arg2;
            }
            this._location = location;
            this._uid = uid;
        }
        /** @inheritdoc */
        get location() {
            return this._location;
        }
        /** @inheritdoc */
        get uid() {
            return this._uid;
        }
        /** @inheritdoc */
        get type() {
            return exports.WaypointTypes.Custom;
        }
    }
    /**
     * A basic implementation of {@link FacilityWaypoint}.
     */
    class BasicFacilityWaypoint extends AbstractWaypoint {
        /**
         * Creates a new instance of BasicFacilityWaypoint.
         * @param facility The facility associated with this waypoint.
         * @param bus The event bus.
         */
        constructor(facility, bus) {
            super();
            this.bus = bus;
            /** @inheritdoc */
            this.isFacilityWaypoint = true;
            this._facility = Subject.create(facility);
            this._location = GeoPointSubject.create(new GeoPoint(facility.lat, facility.lon));
            this._type = BasicFacilityWaypoint.getType(facility);
            const facType = ICAO.getFacilityTypeFromValue(facility.icaoStruct);
            if (facType === exports.FacilityType.VIS || facType === exports.FacilityType.USR) {
                // These types of facilities can be mutated. So we need to listen to the event bus for change events and respond
                // accordingly.
                this.facChangeSub = this.bus.getSubscriber()
                    .on(`facility_changed_${ICAO.getUid(facility.icaoStruct)}`)
                    .handle(newFacility => {
                    this._facility.set(newFacility);
                    this._location.set(newFacility.lat, newFacility.lon);
                });
            }
        }
        /** @inheritdoc */
        get location() {
            return this._location;
        }
        /** @inheritdoc */
        get uid() {
            return ICAO.getUid(this.facility.get().icaoStruct);
        }
        /** @inheritdoc */
        get type() {
            return this._type;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The facility associated with this waypoint.
         */
        get facility() {
            return this._facility;
        }
        /**
         * Gets a waypoint type from a facility.
         * @param facility A facility.
         * @returns The waypoint type corresponding to the facility.
         */
        static getType(facility) {
            switch (ICAO.getFacilityTypeFromValue(facility.icaoStruct)) {
                case exports.FacilityType.Airport:
                    return exports.WaypointTypes.Airport;
                case exports.FacilityType.Intersection:
                    return exports.WaypointTypes.Intersection;
                case exports.FacilityType.NDB:
                    return exports.WaypointTypes.NDB;
                case exports.FacilityType.RWY:
                    return exports.WaypointTypes.Runway;
                case exports.FacilityType.USR:
                    return exports.WaypointTypes.User;
                case exports.FacilityType.VIS:
                    return exports.WaypointTypes.Visual;
                case exports.FacilityType.VOR:
                    return exports.WaypointTypes.VOR;
                default:
                    return exports.WaypointTypes.User;
            }
        }
    }
    /**
     * A utility class for working with FacilityWaypoint.
     */
    class FacilityWaypointUtils {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static isFacilityWaypoint(waypoint, facilityType) {
            if (waypoint.isFacilityWaypoint !== true) {
                return false;
            }
            return facilityType === undefined || FacilityUtils.isFacilityType(waypoint.facility.get(), facilityType);
        }
    }
    /**
     * A flight path waypoint.
     */
    class FlightPathWaypoint extends AbstractWaypoint {
        /** @inheritdoc */
        get location() {
            return this._location;
        }
        /** @inheritdoc */
        get uid() {
            return this._uid;
        }
        /** @inheritdoc */
        get type() { return exports.WaypointTypes.FlightPlan; }
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(arg1, arg2, arg3, arg4, arg5) {
            super();
            if (typeof arg1 === 'number') {
                this._location = GeoPointSubject.create(new GeoPoint(arg1, arg2));
                this._uid = `${FlightPathWaypoint.UID_PREFIX}_${arg4}`;
                this.leg = arg3;
                this.ident = arg5;
            }
            else {
                this._location = arg1;
                this._uid = `${FlightPathWaypoint.UID_PREFIX}_${arg3}`;
                this.leg = arg2;
                this.ident = arg4;
            }
        }
    }
    FlightPathWaypoint.UID_PREFIX = 'FLPTH';
    /**
     * A VNAV waypoint.
     */
    class VNavWaypoint extends AbstractWaypoint {
        /** @inheritdoc */
        get type() { return exports.WaypointTypes.VNAV; }
        /** @inheritdoc */
        get location() {
            return this._location;
        }
        /** @inheritdoc */
        get uid() {
            return this._uid;
        }
        /**
         * Constructor.
         * @param leg The leg that the VNAV waypoint is contained in.
         * @param distanceFromEnd The distance along the flight path from the end of the leg to the location of the waypoint,
         * in meters.
         * @param uid A unique ID to assign to the VNAV waypoint.
         * @param ident This waypoint's ident string.
         */
        constructor(leg, distanceFromEnd, uid, ident) {
            super();
            this.ident = ident;
            this._uid = uid;
            this._location = GeoPointSubject.create(this.getWaypointLocation(leg, distanceFromEnd, new GeoPoint(0, 0)));
        }
        /**
         * Sets this waypoint's location.
         * @param leg The leg that the waypoint resides in.
         * @param distanceFromEnd The distance along the flight path from the end of the leg to the location of the waypoint,
         * in meters.
         */
        setLocation(leg, distanceFromEnd) {
            this._location.set(this.getWaypointLocation(leg, distanceFromEnd, VNavWaypoint.geoPointCache[0]));
        }
        /**
         * Gets the waypoint's location in space.
         * @param leg The leg that the waypoint resides in.
         * @param distanceFromEnd The distance along the flight path from the end of the leg to the location of the waypoint,
         * in meters.
         * @param out The GeoPoint object to which to write the location.
         * @returns The waypoint's location.
         */
        getWaypointLocation(leg, distanceFromEnd, out) {
            var _a, _b;
            if (leg.calculated !== undefined) {
                const vectors = [...leg.calculated.ingress, ...leg.calculated.ingressToEgress, ...leg.calculated.egress];
                let vectorIndex = vectors.length - 1;
                while (vectorIndex >= 0) {
                    const vector = vectors[vectorIndex];
                    const vectorDistance = vector.distance;
                    if (vectorDistance >= distanceFromEnd) {
                        const end = GeoPoint.sphericalToCartesian(vector.endLat, vector.endLon, VNavWaypoint.vec3Cache[0]);
                        return FlightPathUtils.setGeoCircleFromVector(vector, VNavWaypoint.geoCircleCache[0])
                            .offsetDistanceAlong(end, UnitType.METER.convertTo(-distanceFromEnd, UnitType.GA_RADIAN), out, Math.PI);
                    }
                    else {
                        distanceFromEnd -= vectorDistance;
                    }
                    vectorIndex--;
                }
                if (vectors.length > 0) {
                    out.set(vectors[0].startLat, vectors[0].startLon);
                }
                else {
                    out.set((_a = leg.calculated.endLat) !== null && _a !== void 0 ? _a : 0, (_b = leg.calculated.endLon) !== null && _b !== void 0 ? _b : 0);
                }
            }
            return out;
        }
    }
    VNavWaypoint.vec3Cache = [new Float64Array(3)];
    VNavWaypoint.geoPointCache = [new GeoPoint(0, 0)];
    VNavWaypoint.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];

    /**
     * A default implementation of {@link FacilityWaypointCache}.
     */
    class DefaultFacilityWaypointCache {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param size The maximum size of this cache.
         */
        constructor(bus, size) {
            this.bus = bus;
            this.size = size;
            this.cache = new Map();
        }
        /** @inheritdoc */
        get(facility) {
            const key = DefaultFacilityWaypointCache.getFacilityKey(facility);
            let existing = this.cache.get(key);
            if (!existing) {
                existing = new BasicFacilityWaypoint(facility, this.bus);
                this.addToCache(key, existing);
            }
            return existing;
        }
        /**
         * Adds a waypoint to this cache. If the size of the cache is greater than the maximum after the new waypoint is
         * added, a waypoint will be removed from the cache in FIFO order.
         * @param key The key of the waypoint to add.
         * @param waypoint The waypoint to add.
         */
        addToCache(key, waypoint) {
            this.cache.set(key, waypoint);
            if (this.cache.size > this.size) {
                this.cache.delete(this.cache.keys().next().value);
            }
        }
        /**
         * Gets a FacilityWaypointCache instance.
         * @param bus The event bus.
         * @returns A FacilityWaypointCache instance.
         */
        static getCache(bus) {
            var _a;
            return (_a = DefaultFacilityWaypointCache.INSTANCE) !== null && _a !== void 0 ? _a : (DefaultFacilityWaypointCache.INSTANCE = new DefaultFacilityWaypointCache(bus, 1000));
        }
        /**
         * Gets the cache key for a facility.
         * @param facility A facility.
         * @returns The cache key for the specified facility.
         */
        static getFacilityKey(facility) {
            if (FacilityUtils.isFacilityType(facility, exports.FacilityType.Intersection) && ICAO.getFacilityType(facility.icao) !== exports.FacilityType.Intersection) {
                return `mismatch.${facility.icao}`;
            }
            return facility.icao;
        }
    }

    /**
     * A binary min-heap. Each element added to the heap is ordered according to the value of an assigned key relative
     * to the keys of the other elements in the heap. The relative values of element keys are defined by a supplied
     * comparator function. Retrieval of the element with the smallest key (minimum element) is performed in constant time.
     * Removal of the minimum element and insertions are performed in logarithmic time. Additionally, this type of heap
     * supports combined insertion and removal operations (in either order) which are slightly more efficient than chaining
     * the two operations separately.
     */
    class BinaryHeap {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of elements contained in this heap. */
        get size() {
            return this.tree.length;
        }
        /**
         * Constructor.
         * @param comparator The function that this heap uses to compare the keys of its elements. The function returns 0 if
         * `a` and `b` share the same key, a negative number if `a` has a lower key than `b`, and a positive number if `a`
         * has a greater key than `b`.
         */
        constructor(comparator) {
            this.comparator = comparator;
            this.tree = [];
        }
        /**
         * Finds the element in this heap with the smallest key.
         * @returns The element in this heap with the smallest key, or undefined if this heap is empty.
         */
        findMin() {
            return this.tree[0];
        }
        /**
         * Removes and returns the element in this heap with the smallest key.
         * @returns The removed element, or undefined if this heap is empty.
         */
        removeMin() {
            if (this.tree.length === 0) {
                return undefined;
            }
            const min = this.tree[0];
            this.swap(0, this.tree.length - 1);
            this.tree.length--;
            this.heapifyDown(0);
            return min;
        }
        /**
         * Inserts an element into this heap.
         * @param element The element to insert.
         * @returns This heap, after the element has been inserted.
         */
        insert(element) {
            this.tree.push(element);
            this.heapifyUp(this.tree.length - 1);
            return this;
        }
        /**
         * Inserts an element into this heap, then removes the element with the smallest key.
         * @param element The element to insert.
         * @returns The removed element.
         */
        insertAndRemoveMin(element) {
            if (this.tree.length === 0 || this.comparator(element, this.tree[0]) <= 0) {
                return element;
            }
            return this.removeMinAndInsert(element);
        }
        /**
         * Removes the element in this heap with the smallest key, then inserts a new element.
         * @param element The element to insert.
         * @returns The removed element, or undefined if this heap was empty before the new element was inserted.
         */
        removeMinAndInsert(element) {
            const min = this.tree[0];
            this.tree[0] = element;
            this.heapifyDown(0);
            return min;
        }
        /**
         * Removes all elements from this heap.
         * @returns This heap, after it has been cleared.
         */
        clear() {
            this.tree.length = 0;
            return this;
        }
        /**
         * Restores the heap property for this heap upwards from a node which potentially violates the property.
         * @param index The index of the node at which to begin the operation.
         */
        heapifyUp(index) {
            let parent = BinaryHeap.parent(index);
            while (parent >= 0 && this.comparator(this.tree[index], this.tree[parent]) < 0) {
                this.swap(parent, index);
                index = parent;
                parent = BinaryHeap.parent(index);
            }
        }
        /**
         * Restores the heap property for this heap downwards from a node which potentially violates the property.
         * @param index The index of the node at which to begin the operation.
         */
        heapifyDown(index) {
            const len = this.tree.length;
            while (index < len) {
                const left = BinaryHeap.left(index);
                const right = BinaryHeap.right(index);
                let needSwapFlags = 0;
                if (left < len && this.comparator(this.tree[index], this.tree[left]) > 0) {
                    needSwapFlags |= 1;
                }
                if (right < len && this.comparator(this.tree[index], this.tree[right]) > 0) {
                    needSwapFlags |= 2;
                }
                if (needSwapFlags === 3) {
                    needSwapFlags = this.comparator(this.tree[left], this.tree[right]) <= 0 ? 1 : 2;
                }
                if (needSwapFlags === 0) {
                    break;
                }
                const swapChild = needSwapFlags === 1 ? left : right;
                this.swap(index, swapChild);
                index = swapChild;
            }
        }
        /**
         * Swaps two nodes in this heap.
         * @param index1 The index of the first node.
         * @param index2 The index of the second node.
         */
        swap(index1, index2) {
            const old1 = this.tree[index1];
            this.tree[index1] = this.tree[index2];
            this.tree[index2] = old1;
        }
        /**
         * Finds the index of a node's parent.
         * @param index the index of the node for which to find the parent.
         * @returns The index of the query node's parent.
         */
        static parent(index) {
            return (index - 1) >> 1;
        }
        /**
         * Finds the index of a node's left child.
         * @param index The index of the node for which to find the child.
         * @returns The index of the query node's left child.
         */
        static left(index) {
            return index * 2 + 1;
        }
        /**
         * Finds the index of a node's right child.
         * @param index The index of the node for which to find the child.
         * @returns The idnex of the query node's right child.
         */
        static right(index) {
            return index * 2 + 2;
        }
    }

    /**
     * A binominal min-heap. Each element added to the heap is ordered according to the value of an assigned key relative
     * to the keys of the other elements in the heap. The relative values of element keys are defined by a supplied
     * comparator function. Retrieval of the element with the smallest key (minimum element) is performed in constant time.
     * Removal of the minimum element and insertions are performed in logarithmic time (amortized to constant time in the
     * case of insertions). Merges are also supported, with destructive merges performed in logarithmic time.
     */
    class BinomialHeap {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of elements contained in this heap. */
        get size() {
            return this._size;
        }
        /**
         * Constructor.
         * @param comparator The function that this heap uses to compare the keys of its elements. The function returns 0 if
         * `a` and `b` share the same key, a negative number if `a` has a lower key than `b`, and a positive number if `a`
         * has a greater key than `b`.
         */
        constructor(comparator) {
            this.comparator = comparator;
            this._size = 0;
        }
        /**
         * Finds the element in this heap with the smallest key.
         * @returns The element in this heap with the smallest key, or undefined if this heap is empty.
         */
        findMin() {
            return this.minimum;
        }
        /**
         * Removes and returns the element in this heap with the smallest key.
         * @returns The removed element, or undefined if this heap is empty.
         */
        removeMin() {
            // find the root containing the minimum element
            let leftSibling = undefined;
            let minNode = this.rootsHead;
            while (minNode && minNode.element !== this.minimum) {
                leftSibling = minNode;
                minNode = minNode.rightSibling;
            }
            if (!minNode) {
                return undefined;
            }
            // Remove the root containing the minimum element from the heap
            if (leftSibling) {
                leftSibling.rightSibling = minNode.rightSibling;
            }
            else {
                this.rootsHead = minNode.rightSibling;
            }
            // Create a heap from the children of the removed root (since siblings in a binominal tree are arranged highest-
            // order first and the roots of a heap are arranged lowest-order first, it suffices to reverse the order of the
            // siblings) and merge it with this heap
            const heap = BinomialHeap.reverseSiblings(minNode.leftmostChild);
            this.rootsHead = this.mergeHeaps(this.rootsHead, heap);
            this.updateMin();
            this._size--;
            return minNode.element;
        }
        /**
         * Inserts an element into this heap.
         * @param element The element to insert.
         * @returns This heap, after the element has been inserted.
         */
        insert(element) {
            const newRoot = {
                order: 0,
                element
            };
            this.rootsHead = this.mergeHeaps(this.rootsHead, newRoot);
            this.updateMin();
            this._size++;
            return this;
        }
        /**
         * Merges this heap with another one. The merge can either be non-destructive or destructive. A non-destructive merge
         * preserves the other heap. A destructive merge clears the other heap. A destructive merge takes O(log N) time
         * while a non-destructive merge takes O(M + log N) time, where N is either the size of this heap or the size of the
         * other heap, whichever is larger, and M is the size of the other heap. The difference stems from the need to copy
         * the other heap in a non-destructive merge. Note that the result of this operation is only valid if the two heaps
         * have equivalent comparator functions.
         * @param other The heap to merge into this one.
         * @param destructive Whether to perform a destructive merge. False by default.
         * @returns This heap, after the merge has been completed.
         */
        merge(other, destructive = false) {
            const otherSize = other.size;
            let toMerge;
            if (destructive) {
                toMerge = other.rootsHead;
                other.clear();
            }
            else {
                toMerge = BinomialHeap.copyTree(other.rootsHead);
            }
            this.rootsHead = this.mergeHeaps(this.rootsHead, toMerge);
            this.updateMin();
            this._size += otherSize;
            return this;
        }
        /**
         * Removes all elements from this heap.
         * @returns This heap, after it has been cleared.
         */
        clear() {
            this.rootsHead = undefined;
            this.minimum = undefined;
            this._size = 0;
            return this;
        }
        /**
         * Updates the pointer to this heap's minimum element.
         */
        updateMin() {
            let root = this.rootsHead;
            let min;
            while (root) {
                if (min === undefined || this.comparator(root.element, min) < 0) {
                    min = root.element;
                }
                root = root.rightSibling;
            }
            this.minimum = min;
        }
        /**
         * Merges two heaps.
         * @param a The lowest-ordered root of the first heap to merge, or undefined for an empty heap.
         * @param b The lowest-ordered root of the second heap to merge, or undefined for an empty heap.
         * @returns The lowest-ordered root of the union of the two input heaps, or undefined if the merged heap is empty.
         */
        mergeHeaps(a, b) {
            if (!a && !b) {
                return undefined;
            }
            else if (!a) {
                return b;
            }
            else if (!b) {
                return a;
            }
            let currentRootA = a;
            let currentRootB = b;
            let merged;
            let previousRootMerged;
            let currentRootMerged;
            // Iterate through the roots of both heaps simultaneously and add roots to the merged heap in ascending tree order.
            // If there is a root collision in the merged heap (two roots of the same order), resolve the collision by merging
            // the colliding roots and adding the merged root to the merged heap in their place.
            // Keep the iteration going while both input heaps still have roots yet to be added to the merged heap, or there
            // is an unresolved root collision.
            while ((currentRootA && currentRootB) || ((currentRootA === null || currentRootA === void 0 ? void 0 : currentRootA.order) === currentRootMerged.order) || ((currentRootB === null || currentRootB === void 0 ? void 0 : currentRootB.order) === currentRootMerged.order)) {
                // Note: At least one of currentRootA and currentRootB must be defined, and if one of them is not defined, then
                // currentRootMerged must be defined.
                let toAdd;
                if (!currentRootB || (currentRootA && currentRootA.order < currentRootB.order)) {
                    toAdd = currentRootA;
                    currentRootA = currentRootA.rightSibling;
                }
                else if (!currentRootA || currentRootB.order < currentRootA.order) {
                    toAdd = currentRootB;
                    currentRootB = currentRootB.rightSibling;
                }
                else {
                    const currentRootASibling = currentRootA.rightSibling;
                    const currentRootBSibling = currentRootB.rightSibling;
                    toAdd = this.mergeTrees(currentRootA, currentRootB);
                    currentRootA = currentRootASibling;
                    currentRootB = currentRootBSibling;
                }
                if (currentRootMerged) {
                    if (currentRootMerged.order === toAdd.order) {
                        toAdd = this.mergeTrees(currentRootMerged, toAdd);
                        if (previousRootMerged) {
                            previousRootMerged.rightSibling = toAdd;
                        }
                        else {
                            merged = toAdd;
                        }
                    }
                    else {
                        previousRootMerged = currentRootMerged;
                        currentRootMerged.rightSibling = toAdd;
                    }
                }
                else {
                    merged = toAdd;
                }
                currentRootMerged = toAdd;
            }
            // At this point at least one of the input heaps has no more roots to be added to the merged heap, and there are
            // guaranteed to be no more root collisions. Therefore, we just append the rest of the roots from the not-exhausted
            // input heap (if one exists) to the end of the root list of the merged heap.
            currentRootMerged.rightSibling = currentRootA !== null && currentRootA !== void 0 ? currentRootA : currentRootB;
            return merged;
        }
        /**
         * Merges two binomial trees of equal order.
         * @param a The root of the first tree to merge.
         * @param b The root of the second tree to merge.
         * @returns The root of the merged tree.
         * @throws Error if the two input trees have different orders.
         */
        mergeTrees(a, b) {
            if (a.order !== b.order) {
                throw new Error(`BinomialHeap: attempted to merge trees of unequal order (${a.order} and ${b.order})`);
            }
            let min, max;
            if (this.comparator(a.element, b.element) <= 0) {
                min = a;
                max = b;
            }
            else {
                min = b;
                max = a;
            }
            max.rightSibling = min.leftmostChild;
            min.leftmostChild = max;
            min.order++;
            return min;
        }
        /**
         * Reverses the order of sibling nodes.
         * @param leftMostSibling The left-most sibling in a set of sibling nodes to reverse.
         * @returns The left-most sibling of the reversed set of siblings (originally the right-most sibling before the
         * reversal).
         */
        static reverseSiblings(leftMostSibling) {
            if (!leftMostSibling) {
                return undefined;
            }
            if (!leftMostSibling.rightSibling) {
                return leftMostSibling;
            }
            const rightSibling = leftMostSibling.rightSibling;
            const reversed = BinomialHeap.reverseSiblings(rightSibling);
            rightSibling.rightSibling = leftMostSibling;
            leftMostSibling.rightSibling = undefined;
            return reversed;
        }
        /**
         * Copies a binomial tree.
         * @param root The root of the tree to copy.
         * @returns The root of the copy.
         */
        static copyTree(root) {
            if (!root) {
                return undefined;
            }
            return {
                order: root.order,
                element: root.element,
                leftmostChild: root.leftmostChild ? BinomialHeap.copyTree(root.leftmostChild) : undefined,
                rightSibling: root.rightSibling ? BinomialHeap.copyTree(root.rightSibling) : undefined
            };
        }
    }

    /**
     * A linearly interpolated N-dimensional lookup table of vectors.
     */
    class LerpVectorLookupTable {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of dimensions in this table. */
        get dimensionCount() {
            return this._dimensionCount;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The length of the vectors in this table. */
        get vectorLength() {
            return this._vectorLength;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(arg1, arg2) {
            var _a, _b;
            this.table = new SortedArray(LerpVectorLookupTable.BREAKPOINT_COMPARATOR);
            if (typeof arg1 === 'number') {
                this._dimensionCount = isFinite(arg1) ? Math.max(0, arg1) : 0;
                this._vectorLength = isFinite(arg2) ? Math.max(0, arg2) : 0;
            }
            else {
                let leastBreakpointDimension = Infinity;
                let leastVectorLength = Infinity;
                for (let i = 0; i < arg1.length; i++) {
                    leastBreakpointDimension = Math.min(leastBreakpointDimension, Math.max(arg1[i].length - 1, 0));
                    leastVectorLength = Math.min(leastVectorLength, (_b = (_a = arg1[i][0]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
                }
                this._dimensionCount = isFinite(leastBreakpointDimension) ? leastBreakpointDimension : 0;
                this._vectorLength = isFinite(leastVectorLength) ? leastVectorLength : 0;
                if (this._dimensionCount > 0) {
                    for (let i = 0; i < arg1.length; i++) {
                        this.insertBreakpoint(arg1[i]);
                    }
                }
            }
            // Create temporary working vectors: we need 2 per dimension.
            this.tempVectors = ArrayUtils.create(this._dimensionCount * 2, () => new Float64Array(this._vectorLength));
        }
        /**
         * Inserts a breakpoint into this table. If the breakpoint has more dimensions than this table, only the first `N`
         * keys of the breakpoint will be used, where `N` is the dimension count of this table.
         * @param breakpoint A breakpoint, as a number array with the value at index 0 followed by the keys for each
         * dimension.
         * @returns This table, after the breakpoint has been inserted.
         * @throws Error if this table has zero dimensions, the breakpoint has fewer dimensions than this table, or the
         * the length of the breakpoint vector is less than this table's vector length property.
         */
        insertBreakpoint(breakpoint) {
            if (this._dimensionCount === 0) {
                throw new Error('LerpVectorLookupTable: cannot insert a breakpoint into a 0-dimensional table');
            }
            if (breakpoint.length - 1 < this._dimensionCount) {
                throw new Error(`LerpVectorLookupTable: cannot insert a ${breakpoint.length - 1}-dimensional breakpoint into a ${this._dimensionCount}-dimensional table`);
            }
            if (breakpoint[0].length < this._vectorLength) {
                throw new Error(`LerpVectorLookupTable: cannot insert a ${breakpoint[0].length}-length vector into a table with vectors of length ${this._vectorLength}`);
            }
            this.insertBreakpointHelper(breakpoint, 0, this.table);
            return this;
        }
        /**
         * Helper method for inserting a breakpoint into this table.
         * @param breakpoint The breakpoint to insert.
         * @param dimension The current dimension being evaluated.
         * @param array The array of dimensional breakpoints into which the breakpoint should be inserted.
         */
        insertBreakpointHelper(breakpoint, dimension, array) {
            const dimensionKey = breakpoint[dimension + 1];
            const query = LerpVectorLookupTable.tempBreakpoint;
            query.key = dimensionKey;
            if (dimension === this._dimensionCount - 1) {
                let match = array.match(query);
                if (!match) {
                    match = { key: dimensionKey, value: breakpoint[0] };
                    array.insert(match);
                }
            }
            else {
                let next = array.match(query);
                if (!next) {
                    array.insert(next = { key: dimensionKey, array: new SortedArray(LerpVectorLookupTable.BREAKPOINT_COMPARATOR) });
                }
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.insertBreakpointHelper(breakpoint, dimension + 1, next.array);
            }
        }
        /**
         * Looks up a vector in this table using a specified key. The returned vector will be linearly interpolated from
         * surrounding breakpoints if the key is not an exact match for any of the table's breakpoints.
         * @param out The vector to which to write the result.
         * @param key The lookup key, as an ordered N-tuple of numbers.
         * @returns The vector corresponding to the specified key.
         * @throws Error if this table has zero dimensions, the key has fewer dimensions than this table, or a vector could
         * not be retrieved.
         */
        get(out, ...key) {
            if (this._dimensionCount === 0) {
                throw new Error('LerpVectorLookupTable: cannot look up a key in a 0-dimensional table');
            }
            if (key.length < this._dimensionCount) {
                throw new Error(`LerpVectorLookupTable: cannot look up a ${key.length}-dimensional key in a ${this._dimensionCount}-dimensional table`);
            }
            const value = this.lookupHelper(key, 0, this.table, out);
            if (value === undefined) {
                throw new Error(`LerpVectorLookupTable: could not retrieve value for key ${key}`);
            }
            return value;
        }
        /**
         * Helper method for looking up a key in this table.
         * @param key The key to look up.
         * @param dimension The current dimension being evaluated.
         * @param lookupArray The array containing breakpoints in the next lower dimension in which to search for the key.
         * @param out The vector to which to write the result.
         * @returns The interpolated value of the key at the specified dimension.
         */
        lookupHelper(key, dimension, lookupArray, out) {
            const dimensionKey = key[dimension];
            const query = LerpVectorLookupTable.tempBreakpoint;
            query.key = dimensionKey;
            const index = lookupArray.matchIndex(query);
            let start;
            let end;
            if (index >= 0) {
                start = lookupArray.peek(index);
                end = start;
            }
            else {
                start = lookupArray.peek(-index - 2);
                end = lookupArray.peek(-index - 1);
                if (!start) {
                    start = end;
                }
                if (!end) {
                    end = start;
                }
            }
            if (!start || !end) {
                return undefined;
            }
            let startValue;
            let endValue;
            if (dimension === this.dimensionCount - 1) {
                startValue = start.value;
                endValue = end.value;
            }
            else {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                startValue = this.lookupHelper(key, dimension + 1, start.array, this.tempVectors[dimension * 2]);
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                endValue = this.lookupHelper(key, dimension + 1, end.array, this.tempVectors[dimension * 2 + 1]);
            }
            if (startValue === undefined || endValue === undefined) {
                return undefined;
            }
            return MathUtils.lerpVector(out, dimensionKey, start.key, end.key, startValue, endValue);
        }
    }
    LerpVectorLookupTable.BREAKPOINT_COMPARATOR = (a, b) => a.key - b.key;
    LerpVectorLookupTable.tempBreakpoint = { key: 0 };

    /**
     * A 3D table for intepolating across multiple dimensions.
     */
    class Table3D {
        constructor() {
            this.data = [];
        }
        /**
         * Adds a range of values to the table.
         * @param x The x value for this range of values.
         * @param values The range of values in [y, z]
         */
        addRange(x, values) {
            values.sort((a, b) => a[0] - b[0]);
            this.data.push([x, values]);
            this.data.sort((a, b) => a[0] - b[0]);
        }
        /**
         * Gets the interpolated value from the table given an x and y position.
         * @param x The x position to interpolate for.
         * @param y The y position to interpolate for.
         * @returns The interpolated number.
         */
        getValue(x, y) {
            if (x <= this.data[0][0]) {
                return this.interpRange(y, this.data[0][1]);
            }
            if (x >= this.data[this.data.length - 1][0]) {
                return this.interpRange(y, this.data[this.data.length - 1][1]);
            }
            for (let i = 0; i < this.data.length; i++) {
                if (this.data[i][0] >= x) {
                    const bottomRange = this.data[i - 1][1];
                    const topRange = this.data[i][1];
                    const bottomZ = this.interpRange(y, bottomRange);
                    const topZ = this.interpRange(y, topRange);
                    return this.interp2d(x, this.data[i - 1][0], this.data[i][0], bottomZ, topZ);
                }
            }
            return NaN;
        }
        /**
         * Interpolates a range of values given a starting y value.
         * @param y The y value to use.
         * @param range The range of values to interpolate over.
         * @returns A resultant interpolated z value.
         */
        interpRange(y, range) {
            if (range.length === 0) {
                return NaN;
            }
            if (y <= range[0][0]) {
                return range[0][1];
            }
            if (y >= range[range.length - 1][0]) {
                return range[range.length - 1][1];
            }
            for (let i = 0; i < range.length; i++) {
                if (range[i][0] >= y) {
                    return this.interp2d(y, range[i - 1][0], range[i][0], range[i - 1][1], range[i][1]);
                }
            }
            return NaN;
        }
        /**
         * Interpolates in two dimensions.
         * @param y The input y value.
         * @param y0 The bottom y value for interpolation.
         * @param y1 The top y value for interpolation.
         * @param z0 The bottom z number for interpolation
         * @param z1 The top z number for interpolation.
         * @returns An interpolated z result given the input y.
         */
        interp2d(y, y0, y1, z0, z1) {
            const yPercent = (y - y0) / (y1 - y0);
            return ((z1 - z0) * yPercent) + z0;
        }
    }

    /**
     * A k-dimensional search tree.
     */
    class KdTree {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of elements in this tree. */
        get size() {
            return this.elements.length;
        }
        /**
         * Constructor.
         * @param dimensionCount The number of dimensions supported by this tree. If this argument is not an integer, it will
         * be truncated to one.
         * @param keyFunc A function which generates keys from elements. Keys are an N-tuple of numbers, where N is equal to
         * the dimension count of this tree.
         * @throws Error if the dimension count is less than 2.
         */
        constructor(dimensionCount, keyFunc) {
            this.keyFunc = keyFunc;
            this.elements = [];
            this.keys = [];
            this.nodes = [];
            this.minDepth = -1;
            this.maxDepth = -1;
            this.dimensionCount = Math.trunc(dimensionCount);
            if (this.dimensionCount < 2) {
                throw new Error(`KdTree: cannot create a tree with ${this.dimensionCount} dimensions.`);
            }
            this.indexArrays = Array.from({ length: this.dimensionCount + 1 }, () => []);
            this.indexSortFuncs = Array.from({ length: this.dimensionCount }, (v, index) => {
                return (a, b) => {
                    const aKey = this.keys[a];
                    const bKey = this.keys[b];
                    for (let i = 0; i < this.dimensionCount; i++) {
                        const dimension = (i + index) % this.dimensionCount;
                        if (aKey[dimension] < bKey[dimension]) {
                            return -1;
                        }
                        else if (aKey[dimension] > bKey[dimension]) {
                            return 1;
                        }
                    }
                    return 0;
                };
            });
            this.keyCache = [
                new Float64Array(this.dimensionCount)
            ];
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        searchKey(key, radius, arg3, out, filter) {
            if (typeof arg3 === 'number') {
                return this.doResultsSearch(undefined, key, radius, arg3, out, filter);
            }
            else {
                this.doVisitorSearch(undefined, key, radius, arg3);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        search(element, radius, arg3, out, filter) {
            const key = this.keyFunc(element, this.keyCache[0]);
            if (typeof arg3 === 'number') {
                return this.doResultsSearch(element, key, radius, arg3, out, filter);
            }
            else {
                this.doVisitorSearch(element, key, radius, arg3);
            }
        }
        /**
         * Performs a tree search with a visitor function.
         * @param element The query element, or undefined if none exists.
         * @param key The query key.
         * @param radius The search radius.
         * @param visitor A visitor function. This function will be called once per element found within the search radius.
         * If the visitor returns `true`, then the search will continue; if the visitor returns `false`, the search will
         * immediately halt.
         */
        doVisitorSearch(element, key, radius, visitor) {
            const resultHandler = (elementIndex, elementInner, keyInner, distance, queryKey, queryElement) => {
                return visitor(elementInner, keyInner, distance, queryKey, queryElement);
            };
            const traversalHandler = (offsetFromPivot, searchRadius, child) => {
                return searchRadius + offsetFromPivot * child >= 0;
            };
            this.searchTree(element, key, radius, 0, 0, resultHandler, traversalHandler);
        }
        /**
         * Performs a tree search and returns an array of search results.
         * @param element The query element, or undefined if none exists.
         * @param key The query key.
         * @param radius The search radius.
         * @param maxResultCount The maximum number of search results to return.
         * @param out An array in which to store the search results.
         * @param filter A function to filter the search results.
         * @returns An array containing the search results, in order of increasing distance from the query key.
         */
        doResultsSearch(element, key, radius, maxResultCount, out, filter) {
            if (maxResultCount <= 0) {
                out.length = 0;
                return out;
            }
            const heap = new BinaryHeap((a, b) => KdTree.distance(key, this.keys[b], this.dimensionCount) - KdTree.distance(key, this.keys[a], this.dimensionCount));
            const resultHandler = (elementIndex, elementInner, keyInner, distance, queryKey, queryElement) => {
                if (!filter || filter(elementInner, keyInner, distance, queryKey, queryElement)) {
                    if (heap.size === maxResultCount) {
                        heap.insertAndRemoveMin(elementIndex);
                    }
                    else {
                        heap.insert(elementIndex);
                    }
                }
                return true;
            };
            const traversalHandler = (offsetFromPivot, searchRadius, child) => {
                let maxDist = searchRadius;
                if (heap.size === maxResultCount) {
                    maxDist = Math.min(maxDist, KdTree.distance(key, this.keys[heap.findMin()], this.dimensionCount));
                }
                return maxDist + offsetFromPivot * child >= 0;
            };
            this.searchTree(element, key, radius, 0, 0, resultHandler, traversalHandler);
            out.length = heap.size;
            for (let i = out.length - 1; i >= 0; i--) {
                out[i] = this.elements[heap.removeMin()];
            }
            return out;
        }
        /**
         * Searches a subtree for elements whose keys are located near a query key.
         * @param element The query element, or undefined if none exists.
         * @param key The query key.
         * @param radius The search radius.
         * @param nodeIndex The index of the root of the subtree to search.
         * @param pivotDimension The dimension in which the root of the subtree is split.
         * @param resultHandler A function which will be called once per element found within the search radius. If the
         * function returns `true`, then the search will continue; if the function returns `false`, the search will
         * immediately halt.
         * @param traversalHandler A function which determines whether the search will proceed to a child node. If the
         * function returns `true`, the search will continue; if the function returns `false`, the search will skip the
         * child.
         * @returns `false` if the search was terminated prematurely by the `resultHandler` function, and `true` otherwise.
         */
        searchTree(element, key, radius, nodeIndex, pivotDimension, resultHandler, traversalHandler) {
            const elementIndex = this.nodes[nodeIndex];
            if (elementIndex === undefined) {
                return true;
            }
            const nodeKey = this.keys[elementIndex];
            const distanceFromNode = KdTree.distance(key, nodeKey, this.dimensionCount);
            if (distanceFromNode <= radius) {
                if (!resultHandler(elementIndex, this.elements[elementIndex], nodeKey, distanceFromNode, key, element)) {
                    return false;
                }
            }
            const offsetFromPivot = key[pivotDimension] - nodeKey[pivotDimension];
            const nextPivotDimension = (pivotDimension + 1) % this.dimensionCount;
            const lesserNodeIndex = KdTree.lesser(nodeIndex);
            const greaterNodeIndex = KdTree.greater(nodeIndex);
            if (this.nodes[lesserNodeIndex] !== undefined && traversalHandler(offsetFromPivot, radius, -1)) {
                if (!this.searchTree(element, key, radius, lesserNodeIndex, nextPivotDimension, resultHandler, traversalHandler)) {
                    return false;
                }
            }
            if (this.nodes[greaterNodeIndex] !== undefined && traversalHandler(offsetFromPivot, radius, 1)) {
                if (!this.searchTree(element, key, radius, greaterNodeIndex, nextPivotDimension, resultHandler, traversalHandler)) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Inserts an element into this tree. This operation will trigger a rebalancing if, after the insertion, the length
         * of this tree's longest branch is more than twice the length of the shortest branch.
         * @param element The element to insert.
         */
        insert(element) {
            const insertDepth = this.insertElementInTree(element) + 1;
            this.maxDepth = Math.max(this.maxDepth, insertDepth);
            if (insertDepth === this.minDepth + 1) {
                this.minDepth = KdTree.depth(this.nodes.indexOf(undefined, KdTree.leastIndexAtDepth(Math.max(0, this.minDepth))));
            }
            // Rebalance the tree if max depth is greater than twice the min depth.
            if (this.maxDepth + 1 > (this.minDepth + 1) * 2) {
                this.rebuild();
            }
        }
        /**
         * Inserts a batch of elements into this tree. This tree will be rebalanced after the elements are inserted.
         * @param elements An iterable of the elements to insert.
         */
        insertAll(elements) {
            for (const element of elements) {
                this.elements.push(element);
                this.keys.push(this.keyFunc(element, new Float64Array(this.dimensionCount)));
                const insertedIndex = this.elements.length - 1;
                for (let i = 0; i < this.dimensionCount; i++) {
                    this.indexArrays[i].push(insertedIndex);
                }
            }
            this.rebuild();
        }
        /**
         * Inserts an element into this tree.
         * @param element The element to insert.
         * @returns The depth at which the element was inserted, with 0 being the depth of the root.
         */
        insertElementInTree(element) {
            const key = this.keyFunc(element, new Float64Array(this.dimensionCount));
            let index = 0;
            let depth = 0;
            let elementIndex;
            while ((elementIndex = this.nodes[index]) !== undefined) {
                const pivotDimension = depth % this.dimensionCount;
                const keyToCompare = key[pivotDimension];
                if (keyToCompare <= this.keys[elementIndex][pivotDimension]) {
                    index = KdTree.lesser(index);
                }
                else {
                    index = KdTree.greater(index);
                }
                depth++;
            }
            this.elements.push(element);
            this.keys.push(key);
            const insertedIndex = this.elements.length - 1;
            this.nodes[index] = insertedIndex;
            for (let i = 0; i < this.dimensionCount; i++) {
                this.indexArrays[i].push(insertedIndex);
            }
            return depth;
        }
        /**
         * Removes an element from this tree. This tree will be rebalanced after the element is removed.
         * @param element The element to remove.
         * @returns Whether the element was removed.
         */
        remove(element) {
            if (!this.removeElementFromArrays(element)) {
                return false;
            }
            this.resetIndexArrays();
            this.rebuild();
            return true;
        }
        /**
         * Removes a batch of elements from this tree. This tree will be rebalanced after the elements are removed.
         * @param elements An iterable of the elements to remove.
         * @returns Whether at least one element was removed.
         */
        removeAll(elements) {
            let removed = false;
            for (const element of elements) {
                removed = this.removeElementFromArrays(element) || removed;
            }
            if (removed) {
                this.resetIndexArrays();
                this.rebuild();
            }
            return removed;
        }
        /**
         * Removes an element and all references to it from this tree's arrays. This method does not change the structure
         * of this tree to reflect the removal of the element.
         * @param element The element to remove.
         * @returns Whether the element was removed.
         */
        removeElementFromArrays(element) {
            const index = this.elements.indexOf(element);
            if (index < 0) {
                return false;
            }
            const lastIndex = this.elements.length - 1;
            this.elements[index] = this.elements[lastIndex];
            this.keys[index] = this.keys[lastIndex];
            this.elements.length--;
            this.keys.length--;
            return true;
        }
        /**
         * Resets this tree's index arrays such that each array contains the indexes 0 to N-1 in order, where N is the
         * number of elements in the tree.
         */
        resetIndexArrays() {
            for (let i = 0; i < this.dimensionCount; i++) {
                const array = this.indexArrays[i];
                array.length = this.elements.length;
                for (let j = 0; j < array.length; j++) {
                    array[j] = j;
                }
            }
        }
        /**
         * Removes elements from this tree, then inserts elements into this tree as a single operation. The tree will be
         * rebalanced at the end of the operation.
         *
         * Using this method is more efficient than calling `removeAll()` and `insertAll()` separately.
         * @param toRemove An iterable of the elements to remove.
         * @param toInsert An iterable of the elements to insert.
         */
        removeAndInsert(toRemove, toInsert) {
            let removed = false;
            for (const element of toRemove) {
                removed = this.removeElementFromArrays(element) || removed;
            }
            if (removed) {
                this.resetIndexArrays();
            }
            this.insertAll(toInsert);
        }
        /**
         * Rebuilds and balances this tree.
         */
        rebuild() {
            // clear the tree structure
            this.nodes.length = 0;
            if (this.size === 0) {
                return;
            }
            // sort index arrays
            for (let i = 0; i < this.dimensionCount; i++) {
                this.indexArrays[i].sort(this.indexSortFuncs[i]);
            }
            this.buildSubTree(0, 0, 0, this.indexArrays[0].length);
            const log = Math.log2(this.elements.length + 1);
            this.minDepth = Math.floor(log) - 1;
            this.maxDepth = Math.ceil(log) - 1;
        }
        /**
         * Builds a portion of this tree starting from a specified node using the element indexes stored in a specified
         * section of this tree's index arrays. The built subtree is guaranteed to be balanced. Before calling this method,
         * the index array at position 0 should contain keys sorted in the specified pivot dimension, the array at position
         * 1 should contain keys sorted in the dimension after the pivot dimension, etc (with the dimension wrapping back to
         * 0 when reaching `this.dimensionCount`).
         * @param nodeIndex The index of the tree node at which to start building the tree. The element associated with the
         * pivot key will be placed at this node.
         * @param pivotDimension The dimension in which to split the first level of the tree built by this method.
         * @param start The first index, inclusive, of the section of this tree's index arrays to use to build the tree.
         * @param end The last index, exclusive, of the section of this tree's index arrays to use to build the tree.
         */
        buildSubTree(nodeIndex, pivotDimension, start, end) {
            const tempArray = this.indexArrays[this.dimensionCount];
            const sortedArray = this.indexArrays[0];
            const medianIndex = Math.trunc((start + end) / 2);
            const medianKeyIndex = sortedArray[medianIndex];
            // Insert median into its position in the tree
            this.nodes[nodeIndex] = medianKeyIndex;
            if (end - start === 1) {
                return;
            }
            if (end - start <= 3) {
                const lesserIndex = medianIndex - 1;
                const greaterIndex = medianIndex + 1;
                if (lesserIndex >= start) {
                    this.nodes[KdTree.lesser(nodeIndex)] = sortedArray[lesserIndex];
                }
                if (greaterIndex < end) {
                    this.nodes[KdTree.greater(nodeIndex)] = sortedArray[greaterIndex];
                }
                return;
            }
            for (let i = start; i < end; i++) {
                tempArray[i] = sortedArray[i];
            }
            // Partition the index arrays not in the pivot dimension around the median key in the pivot dimension and at the
            // same time rotate the index arrays such that the index array sorted in the next pivot dimension is located at
            // index 0.
            for (let i = 1; i < this.dimensionCount; i++) {
                const targetArray = this.indexArrays[i - 1];
                const toPartitionArray = this.indexArrays[i];
                let lesserCount = 0;
                let greaterCount = 0;
                for (let j = start; j < end; j++) {
                    const keyIndex = toPartitionArray[j];
                    if (keyIndex === medianKeyIndex) {
                        targetArray[medianIndex] = keyIndex;
                    }
                    else {
                        const comparison = this.indexSortFuncs[pivotDimension](keyIndex, medianKeyIndex);
                        if (comparison <= 0) {
                            const index = start + (lesserCount++);
                            targetArray[index] = keyIndex;
                        }
                        else {
                            const index = medianIndex + 1 + (greaterCount++);
                            targetArray[index] = keyIndex;
                        }
                    }
                }
            }
            // Copy the temporary array (now containing the sorted indexes in the pivot dimension) to the last index array.
            const newSortedArray = this.indexArrays[this.dimensionCount - 1];
            for (let i = start; i < end; i++) {
                newSortedArray[i] = tempArray[i];
            }
            const nextPivotDimension = (pivotDimension + 1) % this.dimensionCount;
            this.buildSubTree(KdTree.lesser(nodeIndex), nextPivotDimension, start, medianIndex);
            this.buildSubTree(KdTree.greater(nodeIndex), nextPivotDimension, medianIndex + 1, end);
        }
        /**
         * Removes all elements from this tree.
         */
        clear() {
            this.elements.length = 0;
            this.keys.length = 0;
            this.nodes.length = 0;
            for (let i = 0; i < this.indexArrays.length; i++) {
                this.indexArrays[i].length = 0;
            }
            this.minDepth = -1;
            this.maxDepth = -1;
        }
        /**
         * Finds the index of a node's parent.
         * @param index the index of the node for which to find the parent.
         * @returns The index of the query node's parent.
         */
        static parent(index) {
            return (index - 1) >> 1;
        }
        /**
         * Finds the index of a node's lesser child.
         * @param index The index of the node for which to find the child.
         * @returns The index of the query node's lesser child.
         */
        static lesser(index) {
            return index * 2 + 1;
        }
        /**
         * Finds the index of a node's greater child.
         * @param index The index of the node for which to find the child.
         * @returns The idnex of the query node's greater child.
         */
        static greater(index) {
            return index * 2 + 2;
        }
        /**
         * Finds the least index of any node located at a given depth.
         * @param depth The depth for which to get the least index. The root of the tree lies at depth 0.
         * @returns The least index of any node located at the specified depth.
         */
        static leastIndexAtDepth(depth) {
            return 1 << depth - 1;
        }
        /**
         * Finds the depth at which a node lies.
         * @param index The index of the node for which to find the depth.
         * @returns The depth at which the node lies. The root of the tree lies at depth 0.
         */
        static depth(index) {
            return Math.trunc(Math.log2(index + 1));
        }
        /**
         * Calculates the Euclidean distance between two keys.
         * @param key1 The first key.
         * @param key2 The second key.
         * @param dimensionCount The number of dimensions in which to calculate the distance.
         * @returns The Euclidean distance between the two keys.
         */
        static distance(key1, key2, dimensionCount) {
            let sumSq = 0;
            for (let i = 0; i < dimensionCount; i++) {
                const diff = key1[i] - key2[i];
                sumSq += diff * diff;
            }
            return Math.sqrt(sumSq);
        }
    }

    /**
     * A spatial tree which is keyed on points on Earth's surface and allows searching for elements based on the great-
     * circle distances from their keys to a query point.
     */
    class GeoKdTree {
        /**
         * Constructor.
         * @param keyFunc A function which generates keys from elements. Keys are cartesian representations of points on
         * Earth's surface.
         * @throws Error if the dimension count is less than 2.
         */
        constructor(keyFunc) {
            this.keyFunc = keyFunc;
            this.cartesianTree = new KdTree(3, (element, out) => {
                const vec = this.keyFunc(element, GeoKdTree.vec3Cache[0]);
                out[0] = vec[0];
                out[1] = vec[1];
                out[2] = vec[2];
                return out;
            });
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        search(arg1, arg2, arg3, arg4, arg5, arg6) {
            let center, radius;
            let argA, argB, argC;
            if (typeof arg1 === 'number') {
                center = GeoPoint.sphericalToCartesian(arg1, arg2, GeoKdTree.vec3Cache[1]);
                radius = arg3;
                argA = arg4;
                argB = arg5;
                argC = arg6;
            }
            else if (!(arg1 instanceof Float64Array)) {
                center = GeoPoint.sphericalToCartesian(arg1, GeoKdTree.vec3Cache[1]);
                radius = arg2;
                argA = arg3;
                argB = arg4;
                argC = arg5;
            }
            else {
                center = arg1;
                radius = arg2;
                argA = arg3;
                argB = arg4;
                argC = arg5;
            }
            const radiusCartesian = Math.sqrt(2 * (1 - Math.cos(Utils.Clamp(radius, 0, Math.PI))));
            if (typeof argA === 'number') {
                return this.doResultsSearch(center, radiusCartesian, argA, argB, argC);
            }
            else {
                this.doVisitorSearch(center, radiusCartesian, argA);
            }
        }
        /**
         * Performs a tree search with a visitor function.
         * @param center The query point.
         * @param radiusCartesian The query radius.
         * @param visitor A visitor function. This function will be called once per element found within the search radius.
         * If the visitor returns `true`, then the search will continue; if the visitor returns `false`, the search will
         * immediately halt.
         */
        doVisitorSearch(center, radiusCartesian, visitor) {
            this.cartesianTree.searchKey(center, radiusCartesian, (element, key) => {
                const vec = Vec3Math.set(key[0], key[1], key[2], GeoKdTree.vec3Cache[2]);
                const greatCircleDist = GeoPoint.distance(vec, center);
                return visitor(element, vec, greatCircleDist, center);
            });
        }
        /**
         * Performs a tree search and returns an array of search results.
         * @param center The query point.
         * @param radiusCartesian The query radius.
         * @param maxResultCount The maximum number of search results to return.
         * @param out An array in which to store the search results.
         * @param filter A function to filter the search results.
         * @returns An array containing the search results, in order of increasing distance from the query key.
         */
        doResultsSearch(center, radiusCartesian, maxResultCount, out, filter) {
            const cartesianFilter = filter
                ? (element, key) => {
                    const vec = Vec3Math.set(key[0], key[1], key[2], GeoKdTree.vec3Cache[2]);
                    const greatCircleDist = GeoPoint.distance(vec, center);
                    return filter(element, vec, greatCircleDist, center);
                }
                : undefined;
            return this.cartesianTree.searchKey(center, radiusCartesian, maxResultCount, out, cartesianFilter);
        }
        /**
         * Inserts an element into this tree. This operation will trigger a rebalancing if, after the insertion, the length
         * of this tree's longest branch is more than twice the length of the shortest branch.
         * @param element The element to insert.
         */
        insert(element) {
            this.cartesianTree.insert(element);
        }
        /**
         * Inserts a batch of elements into this tree. This tree will be rebalanced after the elements are inserted.
         * @param elements An iterable of the elements to insert.
         */
        insertAll(elements) {
            this.cartesianTree.insertAll(elements);
        }
        /**
         * Removes an element from this tree. This tree will be rebalanced after the element is removed.
         * @param element The element to remove.
         * @returns Whether the element was removed.
         */
        remove(element) {
            return this.cartesianTree.remove(element);
        }
        /**
         * Removes a batch of elements from this tree. This tree will be rebalanced after the elements are removed.
         * @param elements An iterable of the elements to remove.
         * @returns Whether at least one element was removed.
         */
        removeAll(elements) {
            return this.cartesianTree.removeAll(elements);
        }
        /**
         * Removes elements from this tree, then inserts elements into this tree as a single operation. The tree will be
         * rebalanced at the end of the operation.
         *
         * Using this method is more efficient than calling `removeAll()` and `insertAll()` separately.
         * @param toRemove An iterable of the elements to remove.
         * @param toInsert An iterable of the elements to insert.
         */
        removeAndInsert(toRemove, toInsert) {
            this.cartesianTree.removeAndInsert(toRemove, toInsert);
        }
        /**
         * Rebuilds and balances this tree.
         */
        rebuild() {
            this.cartesianTree.rebuild();
        }
        /**
         * Removes all elements from this tree.
         */
        clear() {
            this.cartesianTree.clear();
        }
    }
    GeoKdTree.vec3Cache = [new Float64Array(3), new Float64Array(3), new Float64Array(3), new Float64Array(3)];

    /**
     * A subscribable which provides a sorted version of a source SubscribableArray.
     */
    class SortedMappedSubscribableArray extends AbstractSubscribableArray {
        /** @inheritdoc */
        get length() {
            return this.sorted.length;
        }
        /**
         * Constructor.
         * @param source The source array subject for this subscribable.
         * @param comparatorFunc A function which defines the relative sorting priority of two elements. The function should
         * return 0 if its arguments are to be sorted identically, a negative number if the first argument is to be sorted
         * before the second argument, and a positive number if the first argument is to be sorted after the second argument.
         * @param equalityFunc A function which checks if two elements are equal. Defaults to the strict equality comparison
         * (`===`) if not defined.
         */
        constructor(source, comparatorFunc, equalityFunc) {
            super();
            this.source = source;
            this.comparatorFunc = comparatorFunc;
            this.equalityFunc = equalityFunc;
            this.sorted = new SortedArray(this.comparatorFunc, this.equalityFunc);
            this.sourceSub = source.sub(this.onSourceChanged.bind(this), true);
        }
        /**
         * Creates a new SortedMappedSubscribableArray.
         * @param source The source array subject for the new mapped sorted array.
         * @param comparatorFunc A function which defines the relative sorting priority of two elements. The function should
         * return 0 if its arguments are to be sorted identically, a negative number if the first argument is to be sorted
         * before the second argument, and a positive number if the first argument is to be sorted after the second argument.
         * @param equalityFunc A function which checks if two elements are equal. Defaults to the strict equality comparison
         * (`===`) if not defined.
         * @returns A new SortedMappedSubscribableArray.
         */
        static create(source, comparatorFunc, equalityFunc) {
            return new SortedMappedSubscribableArray(source, comparatorFunc, equalityFunc);
        }
        /**
         * Responds to changes in this subscribable's source array.
         * @param index The index of the change.
         * @param type The type of change.
         * @param item The item(s) involved in the change, if any.
         */
        onSourceChanged(index, type, item) {
            switch (type) {
                case exports.SubscribableArrayEventType.Cleared:
                    if (this.sorted.length !== 0) {
                        this.sorted.clear();
                        this.notify(0, exports.SubscribableArrayEventType.Cleared);
                    }
                    break;
                case exports.SubscribableArrayEventType.Added:
                    if (item) {
                        this.insert(item);
                    }
                    break;
                case exports.SubscribableArrayEventType.Removed:
                    if (item) {
                        this.remove(item);
                    }
                    break;
            }
        }
        /**
         * Inserts elements into this array.
         * @param elements An element or array of elements to insert.
         */
        insert(elements) {
            if (this.sorted.length === 0) {
                // since we know all elements will be added to one contiguous index range, we can do a small optimization here
                // with notifications
                elements instanceof Array ? this.sorted.insertAll(elements) : this.sorted.insert(elements);
                this.notify(0, exports.SubscribableArrayEventType.Added, elements instanceof Array ? this.sorted.array : elements);
            }
            else {
                if (elements instanceof Array) {
                    const len = elements.length;
                    for (let i = 0; i < len; i++) {
                        const toInsert = elements[i];
                        this.notify(this.sorted.insert(toInsert), exports.SubscribableArrayEventType.Added, toInsert);
                    }
                }
                else {
                    this.notify(this.sorted.insert(elements), exports.SubscribableArrayEventType.Added, elements);
                }
            }
        }
        /**
         * Removes elements from this array.
         * @param elements An element or array of elements to remove.
         */
        remove(elements) {
            if (elements instanceof Array) {
                const len = elements.length;
                for (let i = 0; i < len; i++) {
                    const toRemove = elements[i];
                    const removedIndex = this.sorted.remove(toRemove);
                    if (removedIndex >= 0) {
                        this.notify(removedIndex, exports.SubscribableArrayEventType.Removed, toRemove);
                    }
                }
            }
            else {
                const removedIndex = this.sorted.remove(elements);
                if (removedIndex >= 0) {
                    this.notify(removedIndex, exports.SubscribableArrayEventType.Removed, elements);
                }
            }
        }
        /** @inheritdoc */
        getArray() {
            return this.sorted.array;
        }
        /**
         * Destroys this subscribable. After destruction, this subscribable will no longer update in response to changes
         * made to its source.
         */
        destroy() {
            this.sourceSub.destroy();
        }
    }

    /**
     * A subscribable which provides a filtered version of a source SubscribableArray.
     */
    class FilteredMappedSubscribableArray extends AbstractSubscribableArray {
        /** @inheritdoc */
        get length() {
            return this.filtered.length;
        }
        /**
         * Private constructor for a FilteredMappedSubscribableArray.
         * @param source The source array subject for this subscribable.
         * @param filterFunc The filter function to use to prune members.  Should return false for filtered-out elements.
         */
        constructor(source, filterFunc) {
            super();
            this.source = source;
            this.filterFunc = filterFunc;
            this.filtered = new Array();
            this.sourceSub = source.sub(this.onSourceChanged.bind(this), true);
        }
        /**
         * Public creation method for a new FilteredMappedSubscribableArray.
         * @param source The source array subject for the new mapped array.
         * @param filterFunc The filter function to use to prune members.  Should return false for filtered-out elements.
         * @returns A new SortedMappedSubscribableArray.
         */
        static create(source, filterFunc) {
            return new FilteredMappedSubscribableArray(source, filterFunc);
        }
        /**
         * Responds to changes in this subscribable's source array.
         * @param index The index of the change.
         * @param type The type of change.
         * @param item The item(s) involved in the change, if any.
         */
        onSourceChanged(index, type, item) {
            switch (type) {
                case exports.SubscribableArrayEventType.Cleared:
                    if (this.filtered.length !== 0) {
                        this.filtered = [];
                        this.notify(0, exports.SubscribableArrayEventType.Cleared);
                    }
                    break;
                case exports.SubscribableArrayEventType.Added:
                    if (item) {
                        this.insert(item);
                    }
                    break;
                case exports.SubscribableArrayEventType.Removed:
                    if (item) {
                        this.remove(item);
                    }
                    break;
            }
        }
        /**
         * Set a new filter for this array.
         * @param filterFunc The new filter function.
         */
        setFilter(filterFunc) {
            this.filterFunc = filterFunc;
            this.filtered = [];
            this.notify(0, exports.SubscribableArrayEventType.Cleared);
            this.insert(this.source.getArray());
        }
        /**
         * Takes an element or array of elements and returns an array of only those passing the filter.
         * @param elements An element or array of elements to run through the filter.
         * @returns A new list composed of only those elements which pass the filter.
         */
        filter(elements) {
            if (elements instanceof Array) {
                return elements.filter(this.filterFunc);
            }
            else {
                return this.filterFunc(elements) ? [elements] : [];
            }
        }
        /**
         * Inserts elements into this array.
         * @param elements An element or array of elements to insert.
         */
        insert(elements) {
            const filtered = this.filter(elements);
            if (filtered.length > 0) {
                this.filtered.push(...filtered);
                this.notify(0, exports.SubscribableArrayEventType.Added, filtered);
            }
        }
        /**
         * Finds the index of the first element in this array which equals a query element.
         * @param element The query element.
         * @returns The index of the first matching element, or -1 if no such element.
         */
        searchEquals(element) {
            for (let i = 0; i < this.filtered.length; i++) {
                if (this.filtered[i] === element) {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Removes elements from this array.
         * @param elements An element or array of elements to remove.
         */
        remove(elements) {
            if (elements instanceof Array) {
                for (let i = 0; i < elements.length; i++) {
                    this.removeElement(elements[i]);
                }
            }
            else {
                this.removeElement(elements);
            }
        }
        /**
         * Removes a single element from the array.
         * @param element The element to remove.
         */
        removeElement(element) {
            const removedIndex = this.searchEquals(element);
            if (removedIndex >= 0) {
                this.filtered.splice(removedIndex, 1);
                this.notify(removedIndex, exports.SubscribableArrayEventType.Removed, element);
            }
        }
        /** @inheritdoc */
        getArray() {
            return this.filtered;
        }
        /**
         * Destroys this subscribable. After destruction, this subscribable will no longer update in response to changes
         * made to its source.
         */
        destroy() {
            this.sourceSub.destroy();
        }
    }

    /**
     * A boundary (airspace) with pre-processed LODs.
     */
    class LodBoundary {
        /**
         * Constructor.
         * @param facility This boundary's facility object.
         * @param lodDistanceThresholds The Douglas-Peucker thresholds, in great-arc radians, used by each LOD level. If
         * undefined or an empty array, only one LOD level (LOD0) will be created with a distance threshold of 0.
         * @param lodVectorCountTargets The vector count targets for each LOD level. The number of vectors per shape after
         * simplification will not exceed the LOD levels' target. Non-positive targets are interpreted as unlimited.
         * If undefined, all LOD levels will be assigned an unlimited vector count target.
         */
        constructor(facility, lodDistanceThresholds, lodVectorCountTargets) {
            this.facility = facility;
            this.lodDistanceThresholds = (lodDistanceThresholds === null || lodDistanceThresholds === void 0 ? void 0 : lodDistanceThresholds.length) ? [...lodDistanceThresholds] : [0];
            this.lodVectorCountTargets = lodVectorCountTargets ? [...lodVectorCountTargets] : [];
            this.lods = this.processLods();
        }
        /**
         * Processes this boundary's LOD levels.
         * @returns This boundary's processed LOD levels.
         */
        processLods() {
            var _a;
            const lodShapes = [];
            // Initialize unsimplified shapes
            const originalShapes = this.processShapes(this.facility.vectors);
            if (Array.isArray(this.facility.lods)) {
                //From the sim, LOD0 is always the original shapes
                lodShapes.push(originalShapes);
                //Process the rest of the already simplified sim LODs into shapes
                for (let i = 0; i < this.facility.lods.length; i++) {
                    lodShapes.push(this.processShapes(this.facility.lods[i]));
                }
            }
            else {
                let startIndex = 0;
                if (this.lodDistanceThresholds[0] === 0 && ((_a = this.lodVectorCountTargets[0]) !== null && _a !== void 0 ? _a : 0) <= 0) {
                    // LOD0 has no simplification, so just use the original shapes
                    lodShapes.push(originalShapes);
                    startIndex = 1;
                }
                // Calculate other LOD levels
                const len = this.lodDistanceThresholds.length;
                for (let i = startIndex; i < len; i++) {
                    lodShapes.push(this.simplifyShapes(originalShapes, this.lodDistanceThresholds[i], this.lodVectorCountTargets[i]));
                }
            }
            return lodShapes;
        }
        /**
         * Processes this boundary's facility's vectors into boundary shapes.
         * @param vectors The vectors to process.
         * @returns Boundary shapes corresponding to this boundary's facility's vectors.
         */
        processShapes(vectors) {
            const shapes = [];
            const len = vectors.length;
            let index = 0;
            let currentShape;
            while (index < len) {
                const vector = vectors[index];
                switch (vector.type) {
                    case exports.BoundaryVectorType.Start:
                        currentShape = [];
                        index = this.processShape(currentShape, vectors, index);
                        shapes.push(currentShape);
                        break;
                    case exports.BoundaryVectorType.Circle:
                        currentShape = [];
                        index = this.processCircle(currentShape, vectors, index - 1);
                        shapes.push(currentShape);
                        break;
                }
                index++;
            }
            return shapes;
        }
        /**
         * Processes a single, non-circle boundary shape from this boundary's facility's vectors.
         * @param shape The shape to be processed.
         * @param vectors The array containing the facility boundary vectors making up the shape.
         * @param index The index of the first facility boundary vector which makes up the shape.
         * @returns The index of the last facility boundary vector which makes up the shape.
         */
        processShape(shape, vectors, index) {
            const start = vectors[index++];
            shape.push({ end: new GeoPoint(start.lat, start.lon) });
            const origins = [];
            const len = vectors.length;
            while (index < len) {
                const lastEndpoint = shape[shape.length - 1].end;
                const vector = vectors[index];
                switch (vector.type) {
                    case exports.BoundaryVectorType.Start:
                        break;
                    case exports.BoundaryVectorType.Line:
                        if (!lastEndpoint.equals(vector)) {
                            shape.push({ circle: GeoCircle.createGreatCircle(lastEndpoint, vector), end: new GeoPoint(vector.lat, vector.lon) });
                        }
                        break;
                    case exports.BoundaryVectorType.Origin:
                        origins[vector.originId] = vector;
                        break;
                    case exports.BoundaryVectorType.ArcCCW:
                        {
                            const origin = origins[vector.originId];
                            const end = new GeoPoint(vector.lat, vector.lon);
                            const radius = end.distance(origin);
                            shape.push({ circle: GeoCircle.createFromPoint(origin, radius), end });
                            break;
                        }
                    case exports.BoundaryVectorType.ArcCW:
                        {
                            const origin = origins[vector.originId];
                            const end = new GeoPoint(vector.lat, vector.lon);
                            const circle = GeoCircle.createFromPoint(origin, end.distance(origin)).reverse();
                            shape.push({ circle, end });
                            break;
                        }
                    case exports.BoundaryVectorType.Circle:
                        return index - 1;
                }
                index++;
            }
            return index - 1;
        }
        /**
         * Processes a single circle boundary shape from this boundary's facility's vectors.
         * @param shape The shape to be processed.
         * @param vectors The array containing the facility boundary vectors making up the shape.
         * @param index The index of the first facility boundary vector which makes up the shape.
         * @returns The index of the last facility boundary vector which makes up the shape.
         */
        processCircle(shape, vectors, index) {
            const originVector = vectors[index];
            const circleVector = vectors[index + 1];
            if (originVector.type === exports.BoundaryVectorType.Origin && circleVector.originId === originVector.originId) {
                const radius = UnitType.METER.convertTo(circleVector.radius, UnitType.GA_RADIAN);
                const circle = GeoCircle.createFromPoint(originVector, radius);
                // pick an arbitrary endpoint for the circle.
                const isCenterPole = circle.center[0] === 0 && circle.center[1] === 0 && Math.abs(circle.center[2]) === 1;
                const end = isCenterPole ? new GeoPoint(0, 0) : new GeoPoint(90, 0);
                circle.closest(end, end);
                shape.push({ end });
                shape.push({ circle, end });
            }
            return index + 1;
        }
        /**
         * Simplifies boundary shapes using the Douglas-Peucker algorithm.
         * @param shapes The boundary shapes to simplify.
         * @param distanceThreshold The Douglas-Peucker distance threshold, in great-arc radians.
         * @param vectorCountTarget The vector count target for the simplified shapes. An undefined value is interpreted as
         * an unlimited target.
         * @returns The simplified boundary shapes.
         */
        simplifyShapes(shapes, distanceThreshold, vectorCountTarget) {
            const simplified = [];
            const len = shapes.length;
            for (let i = 0; i < len; i++) {
                simplified.push(this.simplifyShape(shapes[i], distanceThreshold, vectorCountTarget));
            }
            return simplified;
        }
        /**
         * Simplifies a boundary shape using the Douglas-Peucker algorithm.
         * @param shape The boundary shape to simplify.
         * @param distanceThreshold The Douglas-Peucker distance threshold, in great-arc radians.
         * @param vectorCountTarget The vector count target for the simplified shape. An undefined value is interpreted as
         * an unlimited target.
         * @returns The simplified boundary shape.
         */
        simplifyShape(shape, distanceThreshold, vectorCountTarget) {
            if (shape.length < 3) {
                return shape;
            }
            const simplified = [];
            const retain = [];
            retain[0] = true;
            retain[shape.length - 1] = true;
            if (vectorCountTarget === undefined || vectorCountTarget <= 0) {
                this.simplify(distanceThreshold, shape, 0, shape.length, retain);
            }
            else {
                this.simplifyToVectorCount(distanceThreshold, vectorCountTarget, shape, 0, shape.length, retain);
            }
            this.rebuildSimplifiedVectors(shape, retain, simplified);
            return simplified;
        }
        /**
         * Simplifies a sequence of vectors in a boundary shape using the Douglas-Peucker algorithm.
         * @param distanceThreshold The Douglas-Peucker distance threshold, in great-arc radians.
         * @param shape The boundary shape containing the vectors to simplify.
         * @param startIndex The index of the first vector in the sequence to simplify, inclusive.
         * @param endIndex The index of the last vector in the sequence to simplify, exclusive.
         * @param retain An array of boolean values indicating which vectors in the shape to retain after simplification.
         */
        simplify(distanceThreshold, shape, startIndex, endIndex, retain) {
            if (startIndex + 1 >= endIndex) {
                return;
            }
            const start = shape[startIndex].end;
            const end = shape[endIndex - 1].end;
            const reference = start.equals(end) ? start : LodBoundary.geoCircleCache[0].setAsGreatCircle(start, end);
            const farthest = this.findFarthestVector(shape, startIndex + 1, endIndex - 1, reference);
            if (farthest.index < 0) {
                return;
            }
            if (farthest.distance >= distanceThreshold) {
                retain[farthest.index] = true;
                this.simplify(distanceThreshold, shape, startIndex, farthest.index + 1, retain);
                this.simplify(distanceThreshold, shape, farthest.index, endIndex, retain);
            }
        }
        /**
         * Simplifies a sequence of vectors in a boundary shape using the Douglas-Peucker algorithm to a target vector count.
         * @param distanceThreshold The Douglas-Peucker distance threshold, in great-arc radians.
         * @param vectorCountTarget The vector count target for the simplified shape.
         * @param shape The boundary shape containing the vectors to simplify.
         * @param startIndex The index of the first vector in the sequence to simplify, inclusive.
         * @param endIndex The index of the last vector in the sequence to simplify, exclusive.
         * @param retain An array of boolean values indicating which vectors in the shape to retain after simplification.
         */
        simplifyToVectorCount(distanceThreshold, vectorCountTarget, shape, startIndex, endIndex, retain) {
            if (startIndex + 1 >= endIndex) {
                return;
            }
            const queue = LodBoundary.queue;
            queue.clear();
            let edgeIndex = 0;
            let currentStartIndex = startIndex;
            let currentEndIndex = endIndex;
            let currentFarthestIndex;
            let retainCount = 2; // the first and last vectors of a shape are always retained
            while (retainCount < vectorCountTarget) {
                if (currentFarthestIndex === undefined) {
                    this.computeAndInsertEdgeToQueue(distanceThreshold, shape, currentStartIndex, currentEndIndex, queue, edgeIndex++);
                }
                else if (currentFarthestIndex >= 0) {
                    this.computeAndInsertEdgeToQueue(distanceThreshold, shape, currentStartIndex, currentFarthestIndex + 1, queue, edgeIndex++);
                    this.computeAndInsertEdgeToQueue(distanceThreshold, shape, currentFarthestIndex, currentEndIndex, queue, edgeIndex++);
                }
                if (queue.size === 0) {
                    break;
                }
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const edge = queue.removeMin();
                currentStartIndex = edge.startIndex;
                currentEndIndex = edge.endIndex;
                currentFarthestIndex = edge.farthestVectorIndex;
                retain[currentFarthestIndex] = true;
                retainCount++;
            }
        }
        /**
         * Computes an edge and inserts it into a priority queue if the distance from the edge to the farthest vector is
         * greater than a specified distance threshold.
         * @param distanceThreshold The Douglas-Peucker distance threshold, in great-arc radians.
         * @param shape The boundary shape containing the vectors to simplify.
         * @param startIndex The index of the vector at the start of the edge.
         * @param endIndex The index of the vector at the end of the edge.
         * @param queue The priority queue into which to insert the edge.
         * @param edgeIndex The index from which to retrieve an edge from the edge cache, if needed.
         */
        computeAndInsertEdgeToQueue(distanceThreshold, shape, startIndex, endIndex, queue, edgeIndex) {
            var _a;
            var _b;
            const start = shape[startIndex].end;
            const end = shape[endIndex - 1].end;
            const reference = start.equals(end) ? start : LodBoundary.geoCircleCache[0].setAsGreatCircle(start, end);
            const farthest = this.findFarthestVector(shape, startIndex + 1, endIndex - 1, reference);
            if (farthest.distance >= distanceThreshold) {
                const edge = (_a = (_b = LodBoundary.edgeCache)[edgeIndex]) !== null && _a !== void 0 ? _a : (_b[edgeIndex] = LodBoundary.createEdge());
                edge.startIndex = startIndex;
                edge.endIndex = endIndex;
                edge.farthestVectorIndex = farthest.index;
                edge.distanceToFarthestVector = farthest.distance;
                queue.insert(edge);
            }
        }
        /**
         * Finds the vector in a boundary shape containing the farthest point from a reference.
         * @param shape The shape containing the vectors to search.
         * @param startIndex The index of the first vector to search, inclusive.
         * @param endIndex The index of the last vector to search, exclusive.
         * @param reference The reference to which to measure distance.
         * @returns The index of the vector containing the farthest point from a reference, and the corresponding distance in great-arc radians.
         */
        // eslint-disable-next-line jsdoc/require-jsdoc
        findFarthestVector(shape, startIndex, endIndex, reference) {
            let maxDistance = 0;
            let index = -1;
            for (let i = startIndex; i < endIndex; i++) {
                const distance = this.getDistanceFromReference(shape, i, reference);
                if (distance > maxDistance) {
                    index = i;
                    maxDistance = distance;
                }
            }
            return { index, distance: maxDistance };
        }
        /**
         * Calculates the maximum distance from a vector in a boundary shape to a reference.
         * @param shape The shape containing the vector to query.
         * @param index The index of the vector to query.
         * @param reference The reference to which to measure the distance.
         * @returns The maximum distance from the vector to the reference, in great-arc radians.
         */
        getDistanceFromReference(shape, index, reference) {
            const vector = shape[index];
            if (!vector.circle) {
                // If the vector has no path (should not actually ever happen), we will just compute the distance to the vector end point.
                return reference instanceof GeoCircle ? Math.abs(reference.distance(vector.end)) : reference.distance(vector.end);
            }
            else {
                /*
                 * We need to find the point along the vector path circle that is the farthest from the reference. This point is
                 * either one of the arc endpoints (of which we only care about the end point, since the start point is
                 * considered part of the previous vector), the projection of the reference point or center of the reference
                 * circle onto the arc circle, or the reflection of the projected point across the arc's center. While the former
                 * is always a potential candidate, the latter two are candidates only if they lie within the arc.
                 */
                const center = reference instanceof GeoCircle ? reference.center : reference;
                const arcStart = shape[index - 1].end;
                const maxAngularDist = vector.circle.angleAlong(arcStart, vector.end, Math.PI);
                const projectedAngularDist = vector.circle.angleAlong(arcStart, center, Math.PI);
                const oppositeAngularDist = (projectedAngularDist + Math.PI) % (2 * Math.PI);
                let arcDistance;
                if (oppositeAngularDist > 0 && oppositeAngularDist < maxAngularDist) {
                    arcDistance = Math.abs(vector.circle.distance(center) + vector.circle.radius * 2);
                    arcDistance = Math.min(arcDistance, 2 * Math.PI - arcDistance);
                }
                else if (projectedAngularDist > 0 && projectedAngularDist < maxAngularDist) {
                    arcDistance = Math.abs(vector.circle.distance(center));
                }
                if (reference instanceof GeoCircle) {
                    return arcDistance ? Math.abs(arcDistance - Math.PI / 2) : Math.abs(reference.distance(vector.end));
                }
                else {
                    return arcDistance !== null && arcDistance !== void 0 ? arcDistance : vector.end.distance(reference);
                }
            }
        }
        /**
         * Rebuilds vectors for a simplified shape.
         * @param shape The original shape.
         * @param retain An array of boolean values indicating which vectors in the shape to retain after simplification.
         * @param simplified The simplified shape to which to add the rebuilt vectors.
         */
        rebuildSimplifiedVectors(shape, retain, simplified) {
            var _a;
            let lastRetained;
            const len = shape.length;
            for (let i = 0; i < len; i++) {
                if (!retain[i]) {
                    continue;
                }
                const currentVector = shape[i];
                if (currentVector) {
                    const prevRetain = retain[i - 1];
                    const prevVector = shape[i - 1];
                    if (!prevRetain) {
                        // The vector immediately before the current vector was pruned, so we need to reconcile the new start point
                        // of the current retained vector.
                        if (lastRetained) {
                            if (currentVector.circle && !currentVector.circle.isGreatCircle()) {
                                // The current retained vector is not a great circle (arc) -> we need to add a great-circle vector before
                                // it so that we maintain the start point of the arc.
                                simplified.push({ circle: GeoCircle.createGreatCircle(lastRetained.end, prevVector.end), end: prevVector.end });
                                simplified.push(LodBoundary.copyVector(currentVector));
                            }
                            else if ((_a = currentVector.circle) === null || _a === void 0 ? void 0 : _a.isGreatCircle()) {
                                // The current retained vector is a great circle -> we need to replace it with another one that begins
                                // at the correct point (the end of the last retained vector).
                                simplified.push({ circle: GeoCircle.createGreatCircle(lastRetained.end, currentVector.end), end: currentVector.end });
                            }
                            else {
                                // The current retained vector is a start vector -> straight copy since start vectors only have an end point.
                                simplified.push(LodBoundary.copyVector(currentVector));
                            }
                        }
                        else {
                            // This case should only happen with the retained start vector at the beginning of a shape.
                            simplified.push({ end: currentVector.end });
                        }
                    }
                    else {
                        // The vector immediately before the current vector was retained
                        simplified.push(LodBoundary.copyVector(currentVector));
                    }
                    lastRetained = currentVector;
                }
            }
        }
        /**
         * Copies a boundary shape vector.
         * @param source The vector to copy.
         * @returns A copy of `source`.
         */
        static copyVector(source) {
            return { circle: source.circle ? new GeoCircle(source.circle.center, source.circle.radius) : undefined, end: source.end.copy() };
        }
        /**
         * Creates an edge.
         * @returns An edge.
         */
        static createEdge() {
            return {
                startIndex: -1,
                endIndex: -1,
                farthestVectorIndex: -1,
                distanceToFarthestVector: 0
            };
        }
    }
    LodBoundary.geoCircleCache = [new GeoCircle(new Float64Array(3), 0)];
    LodBoundary.edgeCache = [];
    LodBoundary.queue = new BinaryHeap((a, b) => b.distanceToFarthestVector - a.distanceToFarthestVector);

    /**
     * A cache of LodBoundary objects.
     */
    class LodBoundaryCache {
        /**
         * Constructor.
         * @param size The maximum size of this cache.
         * @param lodDistanceThresholds The Douglas-Peucker distance thresholds, in great-arc radians, for each LOD level
         * used by this cache's LodBoundary objects.
         * @param lodVectorCountTargets The vector count targets for each LOD level used by this cache's LodBoundary objects.
         */
        constructor(size, lodDistanceThresholds, lodVectorCountTargets) {
            this.size = size;
            this.lodDistanceThresholds = lodDistanceThresholds;
            this.lodVectorCountTargets = lodVectorCountTargets;
            this.cache = new Map();
        }
        /**
         * Retrieves a LodBoundary from this cache corresponding to a boundary facility. If the requested LodBoundary does
         * not exist, it will be created and added to this cache.
         * @param facility A boundary facility.
         * @returns The LodBoundary corresponding to `facility`.
         */
        get(facility) {
            const existing = this.cache.get(facility.id);
            if (existing) {
                return existing;
            }
            return this.create(facility);
        }
        /**
         * Creates a new LodBoundary and adds it to this cache.
         * @param facility The facility from which to create the new LodBoundary.
         * @returns The newly created LodBoundary.
         */
        create(facility) {
            const boundary = new LodBoundary(facility, this.lodDistanceThresholds, this.lodVectorCountTargets);
            this.cache.set(facility.id, boundary);
            if (this.cache.size > this.size) {
                this.cache.delete(this.cache.keys().next().value);
            }
            return boundary;
        }
    }

    /**
     * A cache for LodBoundary objects.
     */
    class DefaultLodBoundaryCache {
        /**
         * Gets an instance of DefaultLodBoundaryCache.
         * @returns An instance of DefaultLodBoundaryCache.
         */
        static getCache() {
            var _a;
            return (_a = DefaultLodBoundaryCache.INSTANCE) !== null && _a !== void 0 ? _a : (DefaultLodBoundaryCache.INSTANCE = new LodBoundaryCache(DefaultLodBoundaryCache.SIZE, DefaultLodBoundaryCache.DISTANCE_THRESHOLDS, DefaultLodBoundaryCache.VECTOR_COUNT_TARGETS));
        }
    }
    DefaultLodBoundaryCache.SIZE = 500;
    DefaultLodBoundaryCache.DISTANCE_THRESHOLDS = [0, 0.00003, 0.0001, 0.0003];
    DefaultLodBoundaryCache.VECTOR_COUNT_TARGETS = [500, 300, 200, 100];

    /**
     * Types of ICAO data provided by nearest search sessions.
     */
    exports.NearestIcaoSearchSessionDataType = void 0;
    (function (NearestIcaoSearchSessionDataType) {
        NearestIcaoSearchSessionDataType["Struct"] = "Struct";
        NearestIcaoSearchSessionDataType["StringV1"] = "StringV1";
    })(exports.NearestIcaoSearchSessionDataType || (exports.NearestIcaoSearchSessionDataType = {}));

    /// <reference types="@microsoft/msfs-types/js/common" />
    /// <reference types="@microsoft/msfs-types/js/simplane" />
    /**
     * An airway.
     */
    class AirwayObject {
        /** Builds a Airway
         * @param name - the name of the new airway.
         * @param type - the type of the new airway.
         */
        constructor(name, type) {
            this._waypoints = [];
            this._name = name;
            this._type = type;
        }
        /** @inheritDoc */
        get name() {
            return this._name;
        }
        /** @inheritDoc */
        get type() {
            return this._type;
        }
        /** @inheritDoc */
        get waypoints() {
            return this._waypoints;
        }
        /**
         * Sets the waypoints of this airway.
         * @param waypoints is the array of waypoints.
         */
        set waypoints(waypoints) {
            this._waypoints = waypoints;
        }
    }
    /**
     * A type map of facility type to facility search type.
     */
    const FacilityTypeSearchType = {
        /** Airport facility type. */
        [exports.FacilityType.Airport]: exports.FacilitySearchType.Airport,
        /** Intersection facility type. */
        [exports.FacilityType.Intersection]: exports.FacilitySearchType.Intersection,
        /** NDB facility type. */
        [exports.FacilityType.NDB]: exports.FacilitySearchType.Ndb,
        /** VOR facility type. */
        [exports.FacilityType.VOR]: exports.FacilitySearchType.Vor,
        /** USR facility type. */
        [exports.FacilityType.USR]: exports.FacilitySearchType.User,
        /** Visual facility type. */
        [exports.FacilityType.VIS]: exports.FacilitySearchType.Visual
    };
    /**
     * A class that handles loading facility data from the simulator.
     */
    class FacilityLoader {
        /**
         * Creates an instance of the FacilityLoader.
         * @param facilityRepo A local facility repository.
         * @param onInitialized A callback to call when the facility loader has completed initialization.
         */
        constructor(facilityRepo, onInitialized = () => { }) {
            this.facilityRepo = facilityRepo;
            this.onInitialized = onInitialized;
            if (FacilityLoader.facilityListener === undefined) {
                FacilityLoader.facilityListener = RegisterViewListener('JS_LISTENER_FACILITY', () => {
                    FacilityLoader.facilityListener.on('SendAirport', FacilityLoader.onFacilityReceived);
                    FacilityLoader.facilityListener.on('SendIntersection', FacilityLoader.onFacilityReceived);
                    FacilityLoader.facilityListener.on('SendVor', FacilityLoader.onFacilityReceived);
                    FacilityLoader.facilityListener.on('SendNdb', FacilityLoader.onFacilityReceived);
                    FacilityLoader.facilityListener.on('NearestSearchCompleted', FacilityLoader.onNearestSearchCompleted);
                    FacilityLoader.facilityListener.on('NearestSearchCompletedWithStruct', FacilityLoader.onNearestSearchCompleted);
                    setTimeout(() => FacilityLoader.init(), 2000);
                }, true);
            }
            this.awaitInitialization().then(() => this.onInitialized());
        }
        /**
         * Initializes this facility loader.
         */
        static init() {
            FacilityLoader.isInitialized = true;
            for (const resolve of this.initPromiseResolveQueue) {
                resolve();
            }
            this.initPromiseResolveQueue.length = 0;
        }
        /** @inheritDoc */
        awaitInitialization() {
            if (FacilityLoader.isInitialized) {
                return Promise.resolve();
            }
            else {
                return new Promise(resolve => {
                    FacilityLoader.initPromiseResolveQueue.push(resolve);
                });
            }
        }
        /** @inheritDoc */
        tryGetFacility(type, icao, airportDataFlags) {
            return this._tryGetFacility(type, icao, airportDataFlags);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        async getFacility(type, icao, airportDataFlags) {
            if (typeof icao === 'string') {
                icao = ICAO.stringV1ToValue(icao);
            }
            const result = await this._tryGetFacility(type, icao, airportDataFlags);
            if (result === null) {
                throw new Error(`FacilityLoader: facility could not be retrieved for ICAO ${ICAO.tryValueToStringV2(icao)}`);
            }
            else {
                return result;
            }
        }
        /** @inheritDoc */
        getFacilities(icaos, airportDataFlags) {
            return Promise.all(icaos.map((icao) => {
                if (!ICAO.isValueFacility(icao)) {
                    return null;
                }
                return this._tryGetFacility(ICAO.getFacilityTypeFromValue(icao), icao, airportDataFlags);
            }));
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        getFacilitiesOfType(type, icaos, airportDataFlags) {
            return Promise.all(icaos.map((icao) => {
                if (!ICAO.isValueFacility(icao)) {
                    return null;
                }
                return this._tryGetFacility(type, icao, airportDataFlags);
            }));
        }
        /**
         * Attempts to retrieve a facility.
         * @param type The type of facility to retrieve.
         * @param icao The ICAO of the facility to retrieve.
         * @param airportDataFlags Bitflags describing the requested data to be loaded in the airport facility to retrieve.
         * The retrieved facility (if any) is guaranteed to have *at least* as much data loaded as requested. Ignored if the
         * type of facility to retrieve is not `FacilityType.Airport`. Defaults to `AirportFacilityDataFlags.All`.
         * @returns A Promise which will be fulfilled with the requested facility, or `null` if the facility could not be
         * retrieved.
         */
        async _tryGetFacility(type, icao, airportDataFlags) {
            switch (type) {
                case exports.FacilityType.USR:
                case exports.FacilityType.RWY:
                case exports.FacilityType.VIS:
                    return this.getFacilityFromRepo(type, icao);
                case exports.FacilityType.Airport:
                    return this.getFacilityFromCoherent(type, icao, airportDataFlags !== null && airportDataFlags !== void 0 ? airportDataFlags : exports.AirportFacilityDataFlags.All);
                case exports.FacilityType.VOR:
                case exports.FacilityType.NDB:
                case exports.FacilityType.Intersection:
                    return this.getFacilityFromCoherent(type, icao);
                default:
                    console.warn(`FacilityLoader: facility request for facility type ${type} is unsupported.`);
                    return null;
            }
        }
        /**
         * Retrieves a facility from the local facility repository.
         * @param type The type of facility to retrieve.
         * @param icao The ICAO of the facility to retrieve.
         * @returns A Promise which will be fulfilled with the requested facility, or `null` if the facility could not be
         * retrieved.
         */
        //PM Modif
        // Bug for RWY waypoint of type FA that (see LFRN approach RW28 trans SAMPO)
        // The legs contains a type 8 waypoint with icao RLFLFRNRW28 (1000ft altitude from RW)
        // This makes the facility repos to continuously add the "R LFRNRX28" icao 
        async getFacilityFromRepo(type, icao) {
            const fac = this.facilityRepo.get(icao);
            if (fac) {
                return fac;
            }
            else if (type === exports.FacilityType.RWY) {
                try {
                    const airport = await this.getFacility(exports.FacilityType.Airport, ICAO.value(exports.IcaoType.Airport, '', '', icao.airport));
                    const runway = RunwayUtils.matchOneWayRunwayFromIdent(airport, icao.ident);
                    if (runway) {
                        const runwayFac = RunwayUtils.createRunwayFacility(airport, runway);
                        // Check again the repository
                        // fac = this.facilityRepo.get(runwayFac.icaoStruct);
                        // if (fac) {
                        //     return fac;
                        // }
                        this.facilityRepo.add(runwayFac);
                        return runwayFac;
                    }
                }
                catch (e) {
                    // noop
                }
            }
//PM Modif do not create polution to the console with that
//            console.warn(`FacilityLoader: facility ${ICAO.tryValueToStringV2(icao)} could not be found.`);
            return null;
        }
        //PM Modif End
        /**
         * Retrieves a facility from Coherent.
         * @param type The type of facility to retrieve.
         * @param icao The ICAO of the facility to retrieve.
         * @param airportDataFlags Bitflags describing the requested data to be loaded in the airport facility to retrieve.
         * The retrieved facility (if any) is guaranteed to have *at least* as much data loaded as requested. Ignored if the
         * type of facility to retrieve is not `FacilityType.Airport`. Defaults to `0`.
         * @returns A Promise which will be fulfilled with the requested facility, or `null` if the facility could not be
         * retrieved.
         */
        async getFacilityFromCoherent(type, icao, airportDataFlags = 0) {
            const isMismatch = ICAO.getFacilityTypeFromValue(icao) !== type;
            let queue = FacilityLoader.requestQueue;
            let cache = FacilityLoader.facCache;
            if (isMismatch) {
                queue = FacilityLoader.mismatchRequestQueue;
                cache = FacilityLoader.typeMismatchFacCache;
            }
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const uid = ICAO.getUid(icao);
            const cachedFac = cache.get(uid);
            if (cachedFac !== undefined) {
                // If the requested facility is an airport and a cached facility exists, then check if it is missing any of the
                // requested loaded data. If so, then request a new facility from Coherent that includes the requested loaded
                // data AND all the data loaded into the currently cached facility. In all other cases, return the cached
                // facility.
                if (type === exports.FacilityType.Airport && !BitFlags.isAll(cachedFac.loadedDataFlags, airportDataFlags)) {
                    airportDataFlags |= cachedFac.loadedDataFlags;
                }
                else {
                    return cachedFac;
                }
            }
            const currentTime = Date.now();
            const request = queue.get(uid);
            if (request === undefined || currentTime - request.timeStamp > 10000) {
                if (request !== undefined) {
                    console.warn(`FacilityLoader: facility request for ${ICAO.tryValueToStringV2(icao)} has timed out.`);
                    request.resolve(null);
                }
                return this.dispatchCoherentFacilityRequest(queue, undefined, type, icao, airportDataFlags);
            }
            else if (type === exports.FacilityType.Airport) {
                airportDataFlags !== null && airportDataFlags !== void 0 ? airportDataFlags : (airportDataFlags = 0);
                if (!BitFlags.isAll(request.airportDataFlags, airportDataFlags)) {
                    return this.dispatchCoherentFacilityRequest(queue, request, type, icao, airportDataFlags | request.airportDataFlags);
                }
            }
            return request.promise;
        }
        /**
         * Dispatches a Coherent facility load request.
         * @param queue The facility request queue in which to insert the request once it has been dispatched.
         * @param existingRequest An existing facility load request that the new request will replace. If defined, then the
         * existing request's Promise will be fulfilled in addition to the new request's Promise when the new request is
         * finished.
         * @param type The type of facility to load.
         * @param icao The ICAO of the facility to load.
         * @param airportDataFlags Bitflags describing the requested data to be loaded in the airport facility to retrieve.
         * The retrieved facility (if any) is guaranteed to have *at least* as much data loaded as requested. Ignored if the
         * type of facility to retrieve is not `FacilityType.Airport`. Defaults to `0`.
         * @returns A Promise which will be fulfilled with the requested facility, or `null` if the facility could not be
         * retrieved.
         */
        dispatchCoherentFacilityRequest(queue, existingRequest, type, icao, airportDataFlags = 0) {
            const uid = ICAO.getUid(icao);
            const isAirport = type === exports.FacilityType.Airport;
            let resolve = undefined;
            const promise = new Promise((resolution) => {
                if (existingRequest) {
                    resolve = (facility) => {
                        existingRequest.resolve(facility);
                        resolution(facility);
                    };
                }
                else {
                    resolve = resolution;
                }
                const coherentCallPromise = isAirport
                    ? Coherent.call(FacilityLoader.coherentLoadFacilityCalls[type], icao, airportDataFlags !== null && airportDataFlags !== void 0 ? airportDataFlags : exports.AirportFacilityDataFlags.All)
                    : Coherent.call(FacilityLoader.coherentLoadFacilityCalls[type], icao);
                coherentCallPromise.then((isValid) => {
                    if (!isValid) {
                        console.warn(`FacilityLoader: facility ${ICAO.tryValueToStringV2(icao)} could not be found.`);
                        resolve(null);
                        queue.delete(uid);
                    }
                });
            });
            const request = { promise, timeStamp: Date.now(), resolve: resolve, airportDataFlags };
            queue.set(uid, request);
            return request.promise;
        }
        /** @inheritDoc */
        async tryGetAirway(airwayName, airwayType, icao) {
            return this._tryGetAirway(airwayName, airwayType, icao);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        async getAirway(airwayName, airwayType, icao) {
            if (typeof icao === 'string') {
                icao = ICAO.stringV1ToValue(icao);
            }
            const airway = await this._tryGetAirway(airwayName, airwayType, icao);
            if (airway) {
                return airway;
            }
            throw new Error(`FacilityLoader: airway ${airwayName} could not be found on waypoint ${ICAO.tryValueToStringV2(icao)}.`);
        }
        /**
         * Attempts to retrieve data for an airway.
         * @param airwayName The airway name.
         * @param airwayType The airway type.
         * @param icao The ICAO value of one intersection in the airway.
         * @returns The retrieved airway data, or `null` if data could not be retrieved.
         */
        async _tryGetAirway(airwayName, airwayType, icao) {
            if (FacilityLoader.airwayCache.has(airwayName)) {
                const cachedAirway = FacilityLoader.airwayCache.get(airwayName);
                const match = cachedAirway === null || cachedAirway === void 0 ? void 0 : cachedAirway.waypoints.find((w) => ICAO.valueEquals(w.icaoStruct, icao));
                if (match !== undefined) {
                    return cachedAirway;
                }
            }
            const fac = await this.getFacility(exports.FacilityType.Intersection, icao);
            const route = fac.routes.find((r) => r.name === airwayName);
            if (route !== undefined) {
                const airwayBuilder = new AirwayBuilder(fac, route, this);
                const status = await airwayBuilder.startBuild();
                if (status === AirwayStatus.COMPLETE) {
                    const waypoints = airwayBuilder.waypoints;
                    if (waypoints !== null) {
                        const airway = new AirwayObject(airwayName, airwayType);
                        airway.waypoints = waypoints;
                        FacilityLoader.addToAirwayCache(airway);
                        return airway;
                    }
                }
            }
            return null;
        }
        /** @inheritDoc */
        async startNearestSearchSessionWithIcaoStructs(type) {
            switch (type) {
                case exports.FacilitySearchType.User:
                case exports.FacilitySearchType.Visual:
                    return this.startRepoNearestSearchSession(type, exports.NearestIcaoSearchSessionDataType.Struct);
                case exports.FacilitySearchType.AllExceptVisual:
                    return this.startCoherentNearestSearchSession(exports.FacilitySearchType.All, exports.NearestIcaoSearchSessionDataType.Struct);
                default:
                    return this.startCoherentNearestSearchSession(type, exports.NearestIcaoSearchSessionDataType.Struct);
            }
        }
        /** @inheritDoc */
        async startNearestSearchSession(type) {
            switch (type) {
                case exports.FacilitySearchType.User:
                case exports.FacilitySearchType.Visual:
                    return this.startRepoNearestSearchSession(type, exports.NearestIcaoSearchSessionDataType.StringV1);
                case exports.FacilitySearchType.AllExceptVisual:
                    return this.startCoherentNearestSearchSession(exports.FacilitySearchType.All, exports.NearestIcaoSearchSessionDataType.StringV1);
                default:
                    return this.startCoherentNearestSearchSession(type, exports.NearestIcaoSearchSessionDataType.StringV1);
            }
        }
        /**
         * Starts a sim-side nearest facilities search session through Coherent.
         * @param type The type of facilities for which to search.
         * @param icaoDataType The data type of the ICAOs provided by the new session. Ignored if the search session does not
         * provide ICAOs.
         * @returns A Promise which will be fulfilled with the new nearest search session.
         */
        async startCoherentNearestSearchSession(type, icaoDataType) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const sessionId = await Coherent.call(icaoDataType === exports.NearestIcaoSearchSessionDataType.Struct ? 'START_NEAREST_SEARCH_SESSION_WITH_STRUCT' : 'START_NEAREST_SEARCH_SESSION', type);
            let session;
            switch (type) {
                case exports.FacilitySearchType.Airport:
                    session = new NearestAirportSearchSession(sessionId, icaoDataType);
                    break;
                case exports.FacilitySearchType.Intersection:
                    session = new NearestIntersectionSearchSession(sessionId, icaoDataType);
                    break;
                case exports.FacilitySearchType.Vor:
                    session = new NearestVorSearchSession(sessionId, icaoDataType);
                    break;
                case exports.FacilitySearchType.Boundary:
                    session = new NearestBoundarySearchSession(sessionId);
                    break;
                default:
                    session = new CoherentIcaoNearestSearchSession(sessionId, icaoDataType);
                    break;
            }
            FacilityLoader.searchSessions.set(sessionId, session);
            return session;
        }
        /**
         * Starts a repository facilities search session.
         * @param type The type of facilities for which to search.
         * @param icaoDataType The data type of the ICAOs provided by the new session.
         * @returns A Promise which will be fulfilled with the new nearest search session.
         * @throws Error if the search type is not supported.
         */
        startRepoNearestSearchSession(type, icaoDataType) {
            // Session ID doesn't really matter for these, so in order to not conflict with IDs from Coherent, we will set
            // them all to negative numbers
            const sessionId = FacilityLoader.repoSearchSessionId--;
            switch (type) {
                case exports.FacilitySearchType.User:
                    return new NearestRepoFacilitySearchSession(this.facilityRepo, sessionId, exports.FacilityType.USR, icaoDataType);
                case exports.FacilitySearchType.Visual:
                    return new NearestRepoFacilitySearchSession(this.facilityRepo, sessionId, exports.FacilityType.VIS, icaoDataType);
                default:
                    throw new Error();
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        async getMetar(arg) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const ident = typeof arg === 'string' ? arg : arg.icaoStruct.ident;
            const metar = await Coherent.call('GET_METAR_BY_IDENT', ident);
            return FacilityLoader.cleanMetar(metar);
        }
        /** @inheritDoc */
        async searchMetar(lat, lon) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const metar = await Coherent.call('GET_METAR_BY_LATLON', lat, lon);
            return FacilityLoader.cleanMetar(metar);
        }
        /**
         * Cleans up a raw METAR object.
         * @param raw A raw METAR object.
         * @returns A cleaned version of the raw METAR object, or undefined if the raw METAR is empty.
         */
        static cleanMetar(raw) {
            var _a;
            if (raw.icao === '') {
                return undefined;
            }
            raw.windSpeed < 0 && delete raw.windSpeed;
            raw.maxWindDir < 0 && delete raw.maxWindDir;
            raw.minWindDir < 0 && delete raw.minWindDir;
            raw.windDir < 0 && delete raw.windDir;
            raw.gust < 0 && delete raw.gust;
            (_a = raw.cavok) !== null && _a !== void 0 ? _a : (raw.cavok = false);
            raw.vis === null && delete raw.vis;
            raw.vertVis === null && delete raw.vertVis;
            raw.temp === -2147483648 && delete raw.temp;
            raw.dew === -2147483648 && delete raw.dew;
            raw.altimeterA < 0 && delete raw.altimeterA;
            raw.altimeterQ < 0 && delete raw.altimeterQ;
            raw.slp < 0 && delete raw.slp;
            return raw;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        async getTaf(arg) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const ident = typeof arg === 'string' ? arg : arg.icaoStruct.ident;
            const taf = await Coherent.call('GET_TAF_BY_IDENT', ident);
            return FacilityLoader.cleanTaf(taf);
        }
        /** @inheritDoc */
        async searchTaf(lat, lon) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            const taf = await Coherent.call('GET_TAF_BY_LATLON', lat, lon);
            return FacilityLoader.cleanTaf(taf);
        }
        /**
         * Cleans up a raw TAF object.
         * @param raw A raw TAF object.
         * @returns A cleaned version of the raw TAF object, or undefined if the raw TAF is empty.
         */
        static cleanTaf(raw) {
            var _a;
            if (raw.icao === '') {
                return undefined;
            }
            for (const group of raw.conditionChangeGroups) {
                group.windSpeed < 0 && delete group.windSpeed;
                group.windDir < 0 && delete group.windDir;
                group.gust < 0 && delete group.gust;
                group.vis === null && delete group.vis;
                group.vertVis === null && delete group.vertVis;
            }
            raw.windSpeed < 0 && delete raw.windSpeed;
            raw.maxWindDir < 0 && delete raw.maxWindDir;
            raw.minWindDir < 0 && delete raw.minWindDir;
            raw.windDir < 0 && delete raw.windDir;
            raw.gust < 0 && delete raw.gust;
            (_a = raw.cavok) !== null && _a !== void 0 ? _a : (raw.cavok = false);
            raw.vis === null && delete raw.vis;
            raw.vertVis === null && delete raw.vertVis;
            raw.altimeterA < 0 && delete raw.altimeterA;
            raw.altimeterQ < 0 && delete raw.altimeterQ;
            raw.slp < 0 && delete raw.slp;
            return raw;
        }
        /** @inheritDoc */
        async searchByIdentWithIcaoStructs(filter, ident, maxItems = 40) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            let results;
            if (filter !== exports.FacilitySearchType.User && filter !== exports.FacilitySearchType.Visual) {
                const coherentFilter = filter === exports.FacilitySearchType.AllExceptVisual ? exports.FacilitySearchType.All : filter;
                results = await Coherent.call('SEARCH_BY_IDENT_WITH_STRUCT', ident, coherentFilter, maxItems);
            }
            else {
                results = [];
            }
            const facRepositorySearchTypes = FacilityLoader.facRepositorySearchTypes[filter];
            if (facRepositorySearchTypes) {
                this.facilityRepo.forEach(fac => {
                    const facIdent = fac.icaoStruct.ident;
                    if (facIdent === ident) {
                        results.unshift(fac.icaoStruct);
                    }
                    else if (facIdent.startsWith(ident)) {
                        results.push(fac.icaoStruct);
                    }
                }, facRepositorySearchTypes);
            }
            return results;
        }
        /** @inheritDoc */
        async searchByIdent(filter, ident, maxItems = 40) {
            if (!FacilityLoader.isInitialized) {
                await this.awaitInitialization();
            }
            let results;
            if (filter !== exports.FacilitySearchType.User && filter !== exports.FacilitySearchType.Visual) {
                const coherentFilter = filter === exports.FacilitySearchType.AllExceptVisual ? exports.FacilitySearchType.All : filter;
                results = await Coherent.call('SEARCH_BY_IDENT', ident, coherentFilter, maxItems);
            }
            else {
                results = [];
            }
            const facRepositorySearchTypes = FacilityLoader.facRepositorySearchTypes[filter];
            if (facRepositorySearchTypes) {
                this.facilityRepo.forEach(fac => {
                    const facIdent = ICAO.getIdent(fac.icao);
                    if (facIdent === ident) {
                        results.unshift(fac.icao);
                    }
                    else if (facIdent.startsWith(ident)) {
                        results.push(fac.icao);
                    }
                }, facRepositorySearchTypes);
            }
            return results;
        }
        /** @inheritDoc */
        async findNearestFacilitiesByIdent(filter, ident, lat, lon, maxItems = 40) {
            const results = await this.searchByIdentWithIcaoStructs(filter, ident, maxItems);
            if (!results) {
                return [];
            }
            const promises = [];
            for (let i = 0; i < results.length; i++) {
                const icao = results[i];
                if (icao.ident === ident) {
                    const facType = ICAO.getFacilityTypeFromValue(icao);
                    promises.push(this.getFacility(facType, icao));
                }
            }
            const foundFacilities = await Promise.all(promises);
            if (foundFacilities.length > 1) {
                foundFacilities.sort((a, b) => GeoPoint.distance(lat, lon, a.lat, a.lon) - GeoPoint.distance(lat, lon, b.lat, b.lon));
                return foundFacilities;
            }
            else if (foundFacilities.length === 1) {
                return foundFacilities;
            }
            else {
                return [];
            }
        }
        /**
         * A callback called when a facility is received from the simulator.
         * @param facility The received facility.
         */
        static onFacilityReceived(facility) {
            const isMismatch = facility['__Type'] === 'JS_FacilityIntersection' && facility.icaoStruct.type !== exports.IcaoType.Waypoint;
            const queue = isMismatch ? FacilityLoader.mismatchRequestQueue : FacilityLoader.requestQueue;
            const uid = ICAO.getUid(facility.icaoStruct);
            const request = queue.get(uid);
            if (request !== undefined) {
                // If the received facility is an airport and it does not have all the loaded data required by the request, then
                // ignore the facility.
                if (FacilityUtils.isFacilityType(facility, exports.FacilityType.Airport) && !BitFlags.isAll(facility.loadedDataFlags, request.airportDataFlags)) {
                    return;
                }
                request.resolve(facility);
                FacilityLoader.addToFacilityCache(facility, isMismatch);
                queue.delete(uid);
            }
        }
        /**
         * A callback called when a search completes.
         * @param results The results of the search.
         */
        static onNearestSearchCompleted(results) {
            const session = FacilityLoader.searchSessions.get(results.sessionId);
            if (session instanceof CoherentNearestSearchSession) {
                session.onSearchCompleted(results);
            }
        }
        /**
         * Adds a facility to the cache.
         * @param fac The facility to add.
         * @param isTypeMismatch Whether to add the facility to the type mismatch cache.
         */
        static addToFacilityCache(fac, isTypeMismatch) {
            const cache = isTypeMismatch ? FacilityLoader.typeMismatchFacCache : FacilityLoader.facCache;
            cache.set(ICAO.getUid(fac.icaoStruct), fac);
            if (cache.size > FacilityLoader.MAX_FACILITY_CACHE_ITEMS) {
                cache.delete(cache.keys().next().value);
            }
        }
        /**
         * Adds an airway to the airway cache.
         * @param airway The airway to add.
         */
        static addToAirwayCache(airway) {
            FacilityLoader.airwayCache.set(airway.name, airway);
            if (FacilityLoader.airwayCache.size > FacilityLoader.MAX_AIRWAY_CACHE_ITEMS) {
                FacilityLoader.airwayCache.delete(FacilityLoader.airwayCache.keys().next().value);
            }
        }
        /**
         * Gets the AIRAC cycles associated with the facility database.
         * @returns an object containing the previous, current, and next cycles.
         * If an error occurs and the MSFS facility cycle cannot be determined, the effective cycle for the current date is used instead.
         */
        static getDatabaseCycles() {
            if (FacilityLoader.databaseCycleCache === undefined) {
                const facilitiesRange = SimVar.GetGameVarValue('FLIGHT NAVDATA DATE RANGE', exports.SimVarValueType.String);
                let current = AiracUtils.parseFacilitiesCycle(facilitiesRange);
                if (current === undefined) {
                    console.error('FacilityLoader: Could not get facility database AIRAC cycle! Falling back to current cycle.');
                    // fall back to current cycle!
                    current = AiracUtils.getCurrentCycle(new Date());
                }
                const previous = AiracUtils.getOffsetCycle(current, -1);
                const next = AiracUtils.getOffsetCycle(current, 1);
                FacilityLoader.databaseCycleCache = {
                    previous,
                    current,
                    next,
                };
            }
            return FacilityLoader.databaseCycleCache;
        }
    }
    FacilityLoader.MAX_FACILITY_CACHE_ITEMS = 1000;
    FacilityLoader.MAX_AIRWAY_CACHE_ITEMS = 1000;
    FacilityLoader.coherentLoadFacilityCalls = {
        [exports.FacilityType.Airport]: 'LOAD_AIRPORT_FROM_STRUCT',
        [exports.FacilityType.VOR]: 'LOAD_VOR_FROM_STRUCT',
        [exports.FacilityType.NDB]: 'LOAD_NDB_FROM_STRUCT',
        [exports.FacilityType.Intersection]: 'LOAD_INTERSECTION_FROM_STRUCT',
    };
    FacilityLoader.requestQueue = new Map();
    FacilityLoader.mismatchRequestQueue = new Map();
    FacilityLoader.facCache = new Map();
    FacilityLoader.typeMismatchFacCache = new Map();
    FacilityLoader.airwayCache = new Map();
    FacilityLoader.searchSessions = new Map();
    FacilityLoader.facRepositorySearchTypes = {
        [exports.FacilitySearchType.All]: [exports.FacilityType.USR, exports.FacilityType.VIS],
        [exports.FacilitySearchType.User]: [exports.FacilityType.USR],
        [exports.FacilitySearchType.Visual]: [exports.FacilityType.VIS],
        [exports.FacilitySearchType.AllExceptVisual]: [exports.FacilityType.USR]
    };
    FacilityLoader.repoSearchSessionId = -1;
    FacilityLoader.isInitialized = false;
    FacilityLoader.initPromiseResolveQueue = [];
    /**
     * A session for searching for nearest facilities through Coherent.
     */
    class CoherentNearestSearchSession {
        /**
         * Creates an instance of a CoherentNearestSearchSession.
         * @param sessionId The ID of the session.
         */
        constructor(sessionId) {
            this.sessionId = sessionId;
            this.searchRequestResolves = new Map();
            this.searchResultQueue = [];
            this.isDequeueActive = false;
        }
        /** @inheritDoc */
        searchNearest(lat, lon, radius, maxItems) {
            const promise = new Promise((resolve) => {
                Coherent.call('SEARCH_NEAREST', this.sessionId, lat, lon, radius, maxItems)
                    .then((searchId) => {
                    this.searchRequestResolves.set(searchId, resolve);
                    this.dequeueResults();
                });
            });
            return promise;
        }
        /**
         * A callback called by the facility loader when a nearest search has completed.
         * @param results The search results.
         */
        onSearchCompleted(results) {
            this.searchResultQueue.push(results);
            this.dequeueResults();
        }
        /**
         * Dequeues this session's received search results. If a Promise has been created for the first result in the queue,
         * then the result is dequeued and the Promise is resolved. This process repeats until the first result in the queue
         * does not have a created Promise or the queue is empty.
         */
        dequeueResults() {
            // Note: we must ensure that Promises are resolved in the same order as that in which the search results are
            // received. This is because search results are returned as "diffs" of the current result relative to the result
            // of the previous search. Search results may be received before a Promise can be created for them. We cannot
            // simply cache results, wait for the Promise to be created, then immediately resolve the Promise. Promises are
            // created in the order in which their associated searches were requested, which is not guaranteed to be the same
            // order in which the results are returned (this is due to the asynchronous nature of the searches inside the sim).
            // Therefore, we need to dequeue the search results in the following manner to ensure Promises are resolved in the
            // correct order.
            if (this.isDequeueActive) {
                return;
            }
            this.isDequeueActive = true;
            while (this.searchResultQueue.length > 0) {
                const queueHead = this.searchResultQueue[0];
                const requestResolve = this.searchRequestResolves.get(queueHead.searchId);
                if (requestResolve) {
                    this.searchResultQueue.shift();
                    this.searchRequestResolves.delete(queueHead.searchId);
                    requestResolve(queueHead);
                }
                else {
                    break;
                }
            }
            this.isDequeueActive = false;
        }
    }
    /**
     * A session for searching for nearest facilities through Coherent that provides ICAOs as results.
     */
    class CoherentIcaoNearestSearchSession extends CoherentNearestSearchSession {
        /**
         * Creates an instance of a CoherentIcaoNearestSearchSession.
         * @param sessionId The ID of the session.
         * @param icaoDataType The data type of the ICAOs provided by this session.
         */
        constructor(sessionId, icaoDataType) {
            super(sessionId);
            this.icaoDataType = icaoDataType;
        }
    }
    /**
     * A session for searching for nearest airports.
     */
    class NearestAirportSearchSession extends CoherentIcaoNearestSearchSession {
        /**
         * Sets the filter for the airport nearest search.
         * @param showClosed Whether or not to show closed airports.
         * @param classMask A bitmask to determine which JS airport classes to show.
         */
        setAirportFilter(showClosed, classMask) {
            Coherent.call('SET_NEAREST_AIRPORT_FILTER', this.sessionId, showClosed ? 1 : 0, classMask);
        }
        /**
         * Sets the extended airport filters for the airport nearest search.
         * @param surfaceTypeMask A bitmask of allowable runway surface types.
         * @param approachTypeMask A bitmask of allowable approach types.
         * @param toweredMask A bitmask of untowered (1) or towered (2) bits.
         * @param minRunwayLength The minimum allowable runway length, in meters.
         */
        setExtendedAirportFilters(surfaceTypeMask, approachTypeMask, toweredMask, minRunwayLength) {
            Coherent.call('SET_NEAREST_EXTENDED_AIRPORT_FILTERS', this.sessionId, surfaceTypeMask, approachTypeMask, toweredMask, minRunwayLength);
        }
    }
    /**
     * Default filters for the nearest airports search session.
     */
    NearestAirportSearchSession.Defaults = {
        ShowClosed: false,
        ClassMask: BitFlags.union(BitFlags.createFlag(exports.AirportClass.HardSurface), BitFlags.createFlag(exports.AirportClass.SoftSurface), BitFlags.createFlag(exports.AirportClass.AllWater), BitFlags.createFlag(exports.AirportClass.HeliportOnly), BitFlags.createFlag(exports.AirportClass.Private)),
        SurfaceTypeMask: 2147483647,
        ApproachTypeMask: 2147483647,
        MinimumRunwayLength: 0,
        ToweredMask: 3
    };
    /**
     * A session for searching for nearest intersections.
     */
    class NearestIntersectionSearchSession extends CoherentIcaoNearestSearchSession {
        /**
         * Sets the filter for the intersection nearest search.
         * @param typeMask A bitmask to determine which JS intersection types to show.
         * @param showTerminalWaypoints Whether or not to show terminal waypoints. Defaults to true.
         */
        setIntersectionFilter(typeMask, showTerminalWaypoints = true) {
            Coherent.call('SET_NEAREST_INTERSECTION_FILTER', this.sessionId, typeMask, showTerminalWaypoints ? 1 : 0);
        }
    }
    /**
     * Default filters for the nearest intersections search session.
     */
    NearestIntersectionSearchSession.Defaults = {
        TypeMask: BitFlags.union(BitFlags.createFlag(exports.IntersectionType.Named), BitFlags.createFlag(exports.IntersectionType.Unnamed), BitFlags.createFlag(exports.IntersectionType.Offroute), BitFlags.createFlag(exports.IntersectionType.IAF), BitFlags.createFlag(exports.IntersectionType.FAF))
    };
    /**
     * A session for searching for nearest VORs.
     */
    class NearestVorSearchSession extends CoherentIcaoNearestSearchSession {
        /**
         * Sets the filter for the VOR nearest search.
         * @param classMask A bitmask to determine which JS VOR classes to show.
         * @param typeMask A bitmask to determine which JS VOR types to show.
         */
        setVorFilter(classMask, typeMask) {
            Coherent.call('SET_NEAREST_VOR_FILTER', this.sessionId, classMask, typeMask);
        }
    }
    /**
     * Default filters for the nearest VORs search session.
     */
    NearestVorSearchSession.Defaults = {
        ClassMask: BitFlags.union(BitFlags.createFlag(exports.VorClass.Terminal), BitFlags.createFlag(exports.VorClass.HighAlt), BitFlags.createFlag(exports.VorClass.LowAlt)),
        TypeMask: BitFlags.union(BitFlags.createFlag(exports.VorType.VOR), BitFlags.createFlag(exports.VorType.DME), BitFlags.createFlag(exports.VorType.VORDME), BitFlags.createFlag(exports.VorType.VORTAC), BitFlags.createFlag(exports.VorType.TACAN))
    };
    /**
     * A session for searching for nearest airspace boundaries.
     */
    class NearestBoundarySearchSession extends CoherentNearestSearchSession {
        /**
         * Sets the filter for the boundary nearest search.
         * @param classMask A bitmask to determine which boundary classes to show.
         */
        setBoundaryFilter(classMask) {
            Coherent.call('SET_NEAREST_BOUNDARY_FILTER', this.sessionId, classMask);
        }
    }
    /**
     * A session for searching for nearest facilities that uses the facility repository.
     */
    class NearestRepoFacilitySearchSession {
        /**
         * Creates an instance of a NearestRepoFacilitySearchSession.
         * @param repo The facility repository in which to search.
         * @param sessionId The ID of this session.
         * @param facilityType The type of facility for which this session searches.
         * @param icaoDataType The data type of the ICAOs provided by this session.
         */
        constructor(repo, sessionId, facilityType, icaoDataType) {
            this.repo = repo;
            this.sessionId = sessionId;
            this.facilityType = facilityType;
            this.icaoDataType = icaoDataType;
            this.filter = undefined;
            this.cachedResults = new Map();
            this.searchId = 0;
        }
        /** @inheritDoc */
        searchNearest(lat, lon, radius, maxItems) {
            const radiusGAR = UnitType.METER.convertTo(radius, UnitType.GA_RADIAN);
            const results = this.repo.search(this.facilityType, lat, lon, radiusGAR, maxItems, [], this.filter);
            const added = [];
            if (this.icaoDataType === exports.NearestIcaoSearchSessionDataType.Struct) {
                for (let i = 0; i < results.length; i++) {
                    const icao = results[i].icaoStruct;
                    const uid = ICAO.getUid(icao);
                    if (this.cachedResults.has(uid)) {
                        this.cachedResults.delete(uid);
                    }
                    else {
                        added.push(icao);
                    }
                }
            }
            else {
                for (let i = 0; i < results.length; i++) {
                    const icao = results[i].icao;
                    if (icao === '') {
                        continue;
                    }
                    if (this.cachedResults.has(icao)) {
                        this.cachedResults.delete(icao);
                    }
                    else {
                        added.push(icao);
                    }
                }
            }
            const removed = Array.from(this.cachedResults.values());
            this.cachedResults.clear();
            if (this.icaoDataType === exports.NearestIcaoSearchSessionDataType.Struct) {
                for (let i = 0; i < results.length; i++) {
                    const result = results[i];
                    this.cachedResults.set(ICAO.getUid(result.icaoStruct), result.icaoStruct);
                }
            }
            else {
                for (let i = 0; i < results.length; i++) {
                    const result = results[i];
                    if (result.icao === '') {
                        continue;
                    }
                    this.cachedResults.set(result.icao, result.icao);
                }
            }
            return Promise.resolve({
                sessionId: this.sessionId,
                searchId: this.searchId++,
                added,
                removed
            });
        }
        /**
         * Sets the filter for this search session.
         * @param filter A function to filter the search results.
         */
        setFacilityFilter(filter) {
            this.filter = filter;
        }
    }
    /**
     * WT Airway Status Enum
     */
    var AirwayStatus;
    (function (AirwayStatus) {
        /**
         * @readonly
         * @property {number} INCOMPLETE - indicates waypoints have not been loaded yet.
         */
        AirwayStatus[AirwayStatus["INCOMPLETE"] = 0] = "INCOMPLETE";
        /**
         * @readonly
         * @property {number} COMPLETE - indicates all waypoints have been successfully loaded.
         */
        AirwayStatus[AirwayStatus["COMPLETE"] = 1] = "COMPLETE";
        /**
         * @readonly
         * @property {number} PARTIAL - indicates some, but not all, waypoints have been successfully loaded.
         */
        AirwayStatus[AirwayStatus["PARTIAL"] = 2] = "PARTIAL";
    })(AirwayStatus || (AirwayStatus = {}));
    /**
     * The Airway Builder.
     */
    class AirwayBuilder {
        /** Creates an instance of the AirwayBuilder
         * @param _initialWaypoint is the initial intersection facility
         * @param _initialData is the intersection route to build from
         * @param facilityLoader is an instance of the facility loader
         */
        constructor(_initialWaypoint, _initialData, facilityLoader) {
            this._initialWaypoint = _initialWaypoint;
            this._initialData = _initialData;
            this.facilityLoader = facilityLoader;
            this._waypointsArray = [];
            this._hasStarted = false;
            this._isDone = false;
        }
        /**
         * Get whether this builder has started loading waypoints
         * @returns whether this builder has started
         */
        get hasStarted() {
            return this._hasStarted;
        }
        /**
         * Get whether this builder is done loading waypoints
         * @returns whether this builder is done loading waypoints
         */
        get isDone() {
            return this._isDone;
        }
        /**
         * Get the airway waypoints
         * @returns the airway waypoints, or null
         */
        get waypoints() {
            return this._waypointsArray;
        }
        /** Steps through the airway waypoints
         * @param stepForward is the direction to step; true = forward, false = backward
         * @param arrayInsertFunc is the arrayInsertFunc
         */
        async _step(stepForward, arrayInsertFunc) {
            let isDone = false;
            let current = this._initialData;
            while (!isDone && current) {
                const nextIcao = stepForward ? current.nextIcaoStruct : current.prevIcaoStruct;
                if (ICAO.isValueFacility(nextIcao)
                    && this._waypointsArray !== null
                    && !this._waypointsArray.find(waypoint => ICAO.valueEquals(waypoint.icaoStruct, nextIcao))) {
                    const fac = await this.facilityLoader.getFacility(exports.FacilityType.Intersection, nextIcao);
                    arrayInsertFunc(fac);
                    const next = fac.routes.find((route) => route.name === current.name);
                    if (next !== undefined) {
                        current = next;
                    }
                    else {
                        isDone = true;
                    }
                }
                else {
                    isDone = true;
                }
            }
        }
        /** Steps Forward through the airway waypoints
         * @returns the step forward function
         */
        async _stepForward() {
            if (this._waypointsArray !== null) {
                return this._step(true, this._waypointsArray.push.bind(this._waypointsArray));
            }
        }
        /** Steps Backward through the airway waypoints
         * @returns the step backward function
         */
        async _stepBackward() {
            if (this._waypointsArray !== null) {
                return this._step(false, this._waypointsArray.unshift.bind(this._waypointsArray));
            }
        }
        /**
         * Begins loading waypoints for this builder's parent airway.
         * @returns a Promise to return a status code corresponding to Airway.Status when this builder has
         * finished loading waypoints.
         */
        startBuild() {
            if (this.hasStarted) {
                return Promise.reject(new Error('Airway builder has already started building.'));
            }
            return new Promise(resolve => {
                this._hasStarted = true;
                if (this._waypointsArray !== null) {
                    this._waypointsArray.push(this._initialWaypoint);
                    Promise.all([
                        this._stepForward(),
                        this._stepBackward()
                    ]).then(() => {
                        this._isDone = true;
                        resolve(AirwayStatus.COMPLETE);
                    }).catch(() => {
                        this._isDone = true;
                        resolve(AirwayStatus.PARTIAL);
                    });
                }
            });
        }
    }

    /**
     * Types of facility repository sync events.
     */
    var FacilityRepositorySyncType;
    (function (FacilityRepositorySyncType) {
        FacilityRepositorySyncType["Add"] = "Add";
        FacilityRepositorySyncType["Remove"] = "Remove";
        FacilityRepositorySyncType["DumpRequest"] = "DumpRequest";
        FacilityRepositorySyncType["DumpResponse"] = "DumpResponse";
    })(FacilityRepositorySyncType || (FacilityRepositorySyncType = {}));
    /**
     * A repository of facilities.
     */
    class FacilityRepository {
        /**
         * Constructor.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.repos = new Map();
            this.trees = {
                [exports.FacilityType.USR]: new GeoKdTree(FacilityRepository.treeKeyFunc),
                [exports.FacilityType.VIS]: new GeoKdTree(FacilityRepository.treeKeyFunc),
            };
            this.ignoreSync = false;
            bus.getSubscriber().on(FacilityRepository.SYNC_TOPIC).handle(this.onSyncEvent.bind(this));
            // Request a dump from any existing instances on other instruments to initialize the repository.
            this.pubSyncEvent({
                type: FacilityRepositorySyncType.DumpRequest, uid: this.lastDumpRequestUid = Math.random() * Number.MAX_SAFE_INTEGER
            });
        }
        /**
         * Gets the number of facilities stored in this repository.
         * @param types The types of facilities to count. Defaults to all facility types.
         * @returns The number of facilities stored in this repository.
         */
        size(types) {
            var _a, _b;
            let size = 0;
            if (types === undefined) {
                for (const repo of this.repos.values()) {
                    size += repo.size;
                }
            }
            else {
                for (let i = 0; i < types.length; i++) {
                    size += (_b = (_a = this.repos.get(types[i])) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0;
                }
            }
            return size;
        }
        /**
         * Retrieves a facility from this repository.
         * @param icao The ICAO of the facility to retrieve.
         * @returns The requested user facility, or undefined if it was not found in this repository.
         */
        get(icao) {
            var _a;
            if (!ICAO.isValueFacility(icao)) {
                return undefined;
            }
            return (_a = this.repos.get(ICAO.getFacilityTypeFromValue(icao))) === null || _a === void 0 ? void 0 : _a.get(ICAO.getUid(icao));
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        search(type, lat, lon, radius, arg5, out, filter) {
            if (type !== exports.FacilityType.USR && type !== exports.FacilityType.VIS) {
                throw new Error(`FacilityRepository: spatial searches are not supported for facility type ${type}`);
            }
            if (typeof arg5 === 'number') {
                return this.trees[type].search(lat, lon, radius, arg5, out, filter);
            }
            else {
                this.trees[type].search(lat, lon, radius, arg5);
            }
        }
        /**
         * Adds a facility to this repository and all other repositories synced with this one. If this repository already
         * contains a facility with the same ICAO as the facility to add, the existing facility will be replaced with the
         * new one.
         * @param fac The facility to add.
         * @throws Error if the facility has an invalid ICAO.
         */
        add(fac) {
            if (!ICAO.isValueFacility(fac.icaoStruct)) {
                throw new Error(`FacilityRepository: invalid facility ICAO ${ICAO.tryValueToStringV2(fac.icaoStruct)}`);
            }
            this.addToRepo(fac);
            this.pubSyncEvent({ type: FacilityRepositorySyncType.Add, facs: [fac] });
        }
        /**
         * Adds multiple facilities from this repository and all other repositories synced with this one. For each added
         * facility, if this repository already contains a facility with the same ICAO, the existing facility will be
         * replaced with the new one.
         * @param facs The facilities to add.
         */
        addMultiple(facs) {
            this.addMultipleToRepo(facs);
            this.pubSyncEvent({ type: FacilityRepositorySyncType.Add, facs: Array.from(facs) });
        }
        /**
         * Removes a facility from this repository and all other repositories synced with this one.
         * @param fac The facility to remove, or the ICAO of the facility to remove.
         * @throws Error if the facility has an invalid ICAO.
         */
        remove(fac) {
            const icao = ICAO.isValue(fac) ? fac : fac.icaoStruct;
            if (!ICAO.isValueFacility(icao)) {
                throw new Error(`FacilityRepository: invalid facility ICAO ${ICAO.tryValueToStringV2(icao)}`);
            }
            this.removeFromRepo(icao);
            this.pubSyncEvent({ type: FacilityRepositorySyncType.Remove, facs: [icao] });
        }
        /**
         * Removes multiple facilities from this repository and all other repositories synced with this one.
         * @param facs The facilities to remove, or the ICAOs of the facilties to remove.
         */
        removeMultiple(facs) {
            this.removeMultipleFromRepo(facs);
            this.pubSyncEvent({ type: FacilityRepositorySyncType.Remove, facs: facs.map(fac => ICAO.isValue(fac) ? fac : fac.icaoStruct) });
        }
        /**
         * Iterates over every facility in this respository with a visitor function.
         * @param fn A visitor function.
         * @param types The types of facilities over which to iterate. Defaults to all facility types.
         */
        forEach(fn, types) {
            var _a;
            if (types === undefined) {
                for (const repo of this.repos.values()) {
                    repo.forEach(fn);
                }
            }
            else {
                for (let i = 0; i < types.length; i++) {
                    (_a = this.repos.get(types[i])) === null || _a === void 0 ? void 0 : _a.forEach(fn);
                }
            }
        }
        /**
         * Adds a facility to this repository.
         * @param fac The facility to add.
         */
        addToRepo(fac) {
            const facilityType = ICAO.getFacilityTypeFromValue(fac.icaoStruct);
            let repo = this.repos.get(facilityType);
            if (repo === undefined) {
                this.repos.set(facilityType, repo = new Map());
            }
            const uid = ICAO.getUid(fac.icaoStruct);
            const existing = repo.get(uid);
            repo.set(uid, fac);
            if (facilityType === exports.FacilityType.USR || facilityType === exports.FacilityType.VIS) {
                if (existing === undefined) {
                    this.trees[facilityType].insert(fac);
                }
                else {
                    this.trees[facilityType].removeAndInsert([existing], [fac]);
                }
            }
            if (existing === undefined) {
                this.publisher.pub('facility_added', fac, false, false);
            }
            else {
                this.publisher.pub(`facility_changed_${uid}`, fac, false, false);
                this.publisher.pub('facility_changed', fac, false, false);
            }
        }
        /**
         * Adds multiple facilities to this repository.
         * @param facs The facilities to add.
         */
        addMultipleToRepo(facs) {
            if (facs.length === 0) {
                return;
            }
            const addedFacilities = [];
            const changedFacilitiesRemoved = [];
            const changedFacilitiesAdded = [];
            for (let i = 0; i < facs.length; i++) {
                const fac = facs[i];
                const facilityType = ICAO.getFacilityTypeFromValue(fac.icaoStruct);
                let repo = this.repos.get(facilityType);
                if (repo === undefined) {
                    this.repos.set(facilityType, repo = new Map());
                }
                const existing = repo.get(ICAO.getUid(fac.icaoStruct));
                repo.set(ICAO.getUid(fac.icaoStruct), fac);
                if (existing === undefined) {
                    addedFacilities.push(fac);
                }
                else {
                    changedFacilitiesRemoved.push(existing);
                    changedFacilitiesAdded.push(fac);
                }
            }
            const addedUserFacilities = facs.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.USR));
            if (addedUserFacilities.length > 0) {
                const removedUserFacilities = changedFacilitiesRemoved.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.USR));
                this.trees[exports.FacilityType.USR].removeAndInsert(removedUserFacilities, addedUserFacilities);
            }
            const addedVisFacilities = facs.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.VIS));
            if (addedVisFacilities.length > 0) {
                const removedVisFacilities = changedFacilitiesRemoved.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.VIS));
                this.trees[exports.FacilityType.VIS].removeAndInsert(removedVisFacilities, addedVisFacilities);
            }
            for (let i = 0; i < addedFacilities.length; i++) {
                const fac = addedFacilities[i];
                this.publisher.pub('facility_added', fac, false, false);
            }
            for (let i = 0; i < changedFacilitiesAdded.length; i++) {
                const fac = changedFacilitiesAdded[i];
                this.publisher.pub(`facility_changed_${ICAO.getUid(fac.icaoStruct)}`, fac, false, false);
                this.publisher.pub('facility_changed', fac, false, false);
            }
        }
        /**
         * Removes a facility from this repository.
         * @param fac The facility to remove, or the ICAO of the facility to remove.
         */
        removeFromRepo(fac) {
            const icao = ICAO.isValue(fac) ? fac : fac.icaoStruct;
            const facilityType = ICAO.getFacilityTypeFromValue(icao);
            const repo = this.repos.get(facilityType);
            if (repo === undefined) {
                return;
            }
            const uid = ICAO.getUid(icao);
            const facilityInRepo = repo.get(uid);
            if (facilityInRepo === undefined) {
                return;
            }
            repo.delete(uid);
            if (facilityType === exports.FacilityType.USR || facilityType === exports.FacilityType.VIS) {
                this.trees[facilityType].remove(facilityInRepo);
            }
            this.publisher.pub(`facility_removed_${uid}`, facilityInRepo, false, false);
            this.publisher.pub('facility_removed', facilityInRepo, false, false);
        }
        /**
         * Removes multiple facilities from this repository.
         * @param facs The facilities to remove, or the ICAOs of the facilities to remove.
         */
        removeMultipleFromRepo(facs) {
            if (facs.length === 0) {
                return;
            }
            const removedFacilities = [];
            for (let i = 0; i < facs.length; i++) {
                const fac = facs[i];
                const icao = ICAO.isValue(fac) ? fac : fac.icaoStruct;
                const repo = this.repos.get(ICAO.getFacilityTypeFromValue(icao));
                if (repo === undefined) {
                    continue;
                }
                const facilityInRepo = repo.get(ICAO.getUid(icao));
                if (facilityInRepo === undefined) {
                    continue;
                }
                repo.delete(ICAO.getUid(icao));
                removedFacilities.push(facilityInRepo);
            }
            const removedUserFacilities = removedFacilities.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.USR));
            if (removedUserFacilities.length > 0) {
                this.trees[exports.FacilityType.USR].removeAll(removedUserFacilities);
            }
            const removedVisFacilities = removedFacilities.filter(fac => FacilityUtils.isFacilityType(fac, exports.FacilityType.VIS));
            if (removedVisFacilities.length > 0) {
                this.trees[exports.FacilityType.VIS].removeAll(removedVisFacilities);
            }
            for (let i = 0; i < removedFacilities.length; i++) {
                const removedFac = removedFacilities[i];
                this.publisher.pub(`facility_removed_${ICAO.getUid(removedFac.icaoStruct)}`, removedFac, false, false);
                this.publisher.pub('facility_removed', removedFac, false, false);
            }
        }
        /**
         * Publishes a facility added or removed sync event over the event bus.
         * @param data The event data.
         */
        pubSyncEvent(data) {
            this.ignoreSync = true;
            this.publisher.pub(FacilityRepository.SYNC_TOPIC, data, true, false);
            this.ignoreSync = false;
        }
        /**
         * A callback which is called when a sync event occurs.
         * @param data The event data.
         */
        onSyncEvent(data) {
            if (this.ignoreSync) {
                return;
            }
            switch (data.type) {
                case FacilityRepositorySyncType.DumpResponse:
                    // Only accept responses to your own dump requests.
                    if (data.uid !== this.lastDumpRequestUid) {
                        break;
                    }
                    else {
                        this.lastDumpRequestUid = undefined;
                    }
                // eslint-disable-next-line no-fallthrough
                case FacilityRepositorySyncType.Add:
                    if (data.facs.length === 1) {
                        this.addToRepo(data.facs[0]);
                    }
                    else {
                        this.addMultipleToRepo(data.facs);
                    }
                    break;
                case FacilityRepositorySyncType.Remove:
                    if (data.facs.length === 1) {
                        this.removeFromRepo(data.facs[0]);
                    }
                    else {
                        this.removeMultipleFromRepo(data.facs);
                    }
                    break;
                case FacilityRepositorySyncType.DumpRequest:
                    // Don't respond to your own dump requests.
                    if (data.uid !== this.lastDumpRequestUid) {
                        const facs = [];
                        this.forEach(fac => facs.push(fac));
                        this.pubSyncEvent({ type: FacilityRepositorySyncType.DumpResponse, uid: data.uid, facs });
                    }
                    break;
            }
        }
        /**
         * Gets an instance of FacilityRepository.
         * @param bus The event bus.
         * @returns an instance of FacilityRepository.
         */
        static getRepository(bus) {
            var _a;
            return (_a = FacilityRepository.INSTANCE) !== null && _a !== void 0 ? _a : (FacilityRepository.INSTANCE = new FacilityRepository(bus));
        }
    }
    FacilityRepository.SYNC_TOPIC = 'facilityrepo_sync';
    FacilityRepository.treeKeyFunc = (fac, out) => {
        return GeoPoint.sphericalToCartesian(fac, out);
    };

    /**
     * Wind speed units used by METAR.
     */
    exports.MetarWindSpeedUnits = void 0;
    (function (MetarWindSpeedUnits) {
        MetarWindSpeedUnits[MetarWindSpeedUnits["Undefined"] = -1] = "Undefined";
        MetarWindSpeedUnits[MetarWindSpeedUnits["Knot"] = 0] = "Knot";
        MetarWindSpeedUnits[MetarWindSpeedUnits["MeterPerSecond"] = 1] = "MeterPerSecond";
        MetarWindSpeedUnits[MetarWindSpeedUnits["KilometerPerHour"] = 2] = "KilometerPerHour";
    })(exports.MetarWindSpeedUnits || (exports.MetarWindSpeedUnits = {}));
    /** Visibility distance units used by METAR. */
    exports.MetarVisibilityUnits = void 0;
    (function (MetarVisibilityUnits) {
        MetarVisibilityUnits[MetarVisibilityUnits["Undefined"] = -1] = "Undefined";
        MetarVisibilityUnits[MetarVisibilityUnits["Meter"] = 0] = "Meter";
        MetarVisibilityUnits[MetarVisibilityUnits["StatuteMile"] = 1] = "StatuteMile";
    })(exports.MetarVisibilityUnits || (exports.MetarVisibilityUnits = {}));
    /**
     * METAR cloud layer coverage/sky condition.
     */
    exports.MetarCloudLayerCoverage = void 0;
    (function (MetarCloudLayerCoverage) {
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["SkyClear"] = 0] = "SkyClear";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["Clear"] = 1] = "Clear";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["NoSignificant"] = 2] = "NoSignificant";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["Few"] = 3] = "Few";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["Scattered"] = 4] = "Scattered";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["Broken"] = 5] = "Broken";
        MetarCloudLayerCoverage[MetarCloudLayerCoverage["Overcast"] = 6] = "Overcast";
    })(exports.MetarCloudLayerCoverage || (exports.MetarCloudLayerCoverage = {}));
    /**
     * METAR significant cloud types.
     */
    exports.MetarCloudLayerType = void 0;
    (function (MetarCloudLayerType) {
        MetarCloudLayerType[MetarCloudLayerType["Unspecified"] = -1] = "Unspecified";
        MetarCloudLayerType[MetarCloudLayerType["ToweringCumulus"] = 0] = "ToweringCumulus";
        MetarCloudLayerType[MetarCloudLayerType["Cumulonimbus"] = 1] = "Cumulonimbus";
        MetarCloudLayerType[MetarCloudLayerType["AltocumulusCastellanus"] = 2] = "AltocumulusCastellanus";
    })(exports.MetarCloudLayerType || (exports.MetarCloudLayerType = {}));
    /** METAR phenomenon types. */
    exports.MetarPhenomenonType = void 0;
    (function (MetarPhenomenonType) {
        MetarPhenomenonType[MetarPhenomenonType["None"] = 0] = "None";
        MetarPhenomenonType[MetarPhenomenonType["Mist"] = 1] = "Mist";
        MetarPhenomenonType[MetarPhenomenonType["Duststorm"] = 2] = "Duststorm";
        MetarPhenomenonType[MetarPhenomenonType["Dust"] = 3] = "Dust";
        MetarPhenomenonType[MetarPhenomenonType["Drizzle"] = 4] = "Drizzle";
        MetarPhenomenonType[MetarPhenomenonType["FunnelCloud"] = 5] = "FunnelCloud";
        MetarPhenomenonType[MetarPhenomenonType["Fog"] = 6] = "Fog";
        MetarPhenomenonType[MetarPhenomenonType["Smoke"] = 7] = "Smoke";
        MetarPhenomenonType[MetarPhenomenonType["Hail"] = 8] = "Hail";
        MetarPhenomenonType[MetarPhenomenonType["SmallHail"] = 9] = "SmallHail";
        MetarPhenomenonType[MetarPhenomenonType["Haze"] = 10] = "Haze";
        MetarPhenomenonType[MetarPhenomenonType["IceCrystals"] = 11] = "IceCrystals";
        MetarPhenomenonType[MetarPhenomenonType["IcePellets"] = 12] = "IcePellets";
        MetarPhenomenonType[MetarPhenomenonType["DustSandWhorls"] = 13] = "DustSandWhorls";
        MetarPhenomenonType[MetarPhenomenonType["Spray"] = 14] = "Spray";
        MetarPhenomenonType[MetarPhenomenonType["Rain"] = 15] = "Rain";
        MetarPhenomenonType[MetarPhenomenonType["Sand"] = 16] = "Sand";
        MetarPhenomenonType[MetarPhenomenonType["SnowGrains"] = 17] = "SnowGrains";
        MetarPhenomenonType[MetarPhenomenonType["Shower"] = 18] = "Shower";
        MetarPhenomenonType[MetarPhenomenonType["Snow"] = 19] = "Snow";
        MetarPhenomenonType[MetarPhenomenonType["Squalls"] = 20] = "Squalls";
        MetarPhenomenonType[MetarPhenomenonType["Sandstorm"] = 21] = "Sandstorm";
        MetarPhenomenonType[MetarPhenomenonType["UnknownPrecip"] = 22] = "UnknownPrecip";
        MetarPhenomenonType[MetarPhenomenonType["VolcanicAsh"] = 23] = "VolcanicAsh";
    })(exports.MetarPhenomenonType || (exports.MetarPhenomenonType = {}));
    /** METAR phenomenon intensities. */
    exports.MetarPhenomenonIntensity = void 0;
    (function (MetarPhenomenonIntensity) {
        MetarPhenomenonIntensity[MetarPhenomenonIntensity["Light"] = -1] = "Light";
        MetarPhenomenonIntensity[MetarPhenomenonIntensity["Normal"] = 0] = "Normal";
        MetarPhenomenonIntensity[MetarPhenomenonIntensity["Heavy"] = 1] = "Heavy";
    })(exports.MetarPhenomenonIntensity || (exports.MetarPhenomenonIntensity = {}));
    /** METAR flight category */
    exports.MetarFlightCategory = void 0;
    (function (MetarFlightCategory) {
        MetarFlightCategory[MetarFlightCategory["Undefined"] = -1] = "Undefined";
        MetarFlightCategory[MetarFlightCategory["LIFR"] = 0] = "LIFR";
        MetarFlightCategory[MetarFlightCategory["IFR"] = 1] = "IFR";
        MetarFlightCategory[MetarFlightCategory["MVFR"] = 2] = "MVFR";
        MetarFlightCategory[MetarFlightCategory["VFR"] = 3] = "VFR";
    })(exports.MetarFlightCategory || (exports.MetarFlightCategory = {}));
    /**
     * TAF condition change group terminologies.
     */
    exports.TafConditionChangeGroupTerminology = void 0;
    (function (TafConditionChangeGroupTerminology) {
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["Undefined"] = -1] = "Undefined";
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["FM"] = 0] = "FM";
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["TEMPO"] = 1] = "TEMPO";
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["PROB"] = 2] = "PROB";
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["BECMG"] = 3] = "BECMG";
        TafConditionChangeGroupTerminology[TafConditionChangeGroupTerminology["RMK"] = 4] = "RMK";
    })(exports.TafConditionChangeGroupTerminology || (exports.TafConditionChangeGroupTerminology = {}));

    /**
     * Possible types of hold entries
     */
    exports.HoldEntryType = void 0;
    (function (HoldEntryType) {
        HoldEntryType[HoldEntryType["Direct"] = 0] = "Direct";
        HoldEntryType[HoldEntryType["Teardrop"] = 1] = "Teardrop";
        HoldEntryType[HoldEntryType["Parallel"] = 2] = "Parallel";
        HoldEntryType[HoldEntryType["None"] = 3] = "None";
    })(exports.HoldEntryType || (exports.HoldEntryType = {}));
    exports.HoldMaxSpeedRule = void 0;
    (function (HoldMaxSpeedRule) {
        HoldMaxSpeedRule[HoldMaxSpeedRule["Faa"] = 0] = "Faa";
        HoldMaxSpeedRule[HoldMaxSpeedRule["Icao"] = 1] = "Icao";
    })(exports.HoldMaxSpeedRule || (exports.HoldMaxSpeedRule = {}));
    /**
     * Utilities for hold entries
     */
    class HoldUtils {
        /**
         * Gets a hold direction UI string for a given inbound course.
         *
         * @param course The inbound course to get the string for.
         * @param short Whether to get the string in short form (single letter)
         *
         * @returns A UI human-readable course string.
         */
        static getDirectionString(course, short = false) {
            if (course >= 0 && course < 22.5) {
                return short ? 'S' : 'South';
            }
            else if (course >= 22.5 && course < 67.5) {
                return short ? 'SW' : 'Southwest';
            }
            else if (course >= 67.5 && course < 112.5) {
                return short ? 'W' : 'West';
            }
            else if (course >= 112.5 && course < 157.5) {
                return short ? 'NW' : 'Northwest';
            }
            else if (course >= 157.5 && course < 202.5) {
                return short ? 'N' : 'North';
            }
            else if (course >= 202.5 && course < 247.5) {
                return short ? 'NE' : 'Northeast';
            }
            else if (course >= 247.5 && course < 292.5) {
                return short ? 'E' : 'East';
            }
            else if (course >= 292.5 && course < 337.5) {
                return short ? 'SE' : 'Southeast';
            }
            else {
                return short ? 'S' : 'South';
            }
        }
        /**
         * Obtains hold speed (number and isMach) depending on altitude and speed rule (ICAO or FAA)
         *
         * @param altitude MSL altitude
         * @param rule     hold speed rule
         *
         * @returns hold speed and whether that number is in Mach
         */
        static getHoldSpeed(altitude, rule) {
            switch (rule) {
                case exports.HoldMaxSpeedRule.Faa:
                    if (altitude < 6000) {
                        return [200, false];
                    }
                    else if (altitude < 14000) {
                        return [230, false];
                    }
                    else {
                        return [265, false];
                    }
                case exports.HoldMaxSpeedRule.Icao:
                    if (altitude < 14000) {
                        return [230, false];
                    }
                    else if (altitude < 20000) {
                        return [240, false];
                    }
                    else if (altitude < 34000) {
                        return [265, false];
                    }
                    else {
                        return [.83, true];
                    }
            }
        }
    }

    /**
     * A type map of search type to concrete facility loader query type.
     */
    const facilitySearchTypeMap = new Map([
        [exports.FacilitySearchType.Airport, exports.FacilityType.Airport],
        [exports.FacilitySearchType.Intersection, exports.FacilityType.Intersection],
        [exports.FacilitySearchType.Vor, exports.FacilityType.VOR],
        [exports.FacilitySearchType.Ndb, exports.FacilityType.NDB],
        [exports.FacilitySearchType.User, exports.FacilityType.USR]
    ]);
    /**
     * A class for tracking a nearest facility session and making it available as a
     * subscribable array of facilities.
     */
    class AbstractNearestSubscription extends AbstractSubscribableArray {
        /**
         * Creates a new instance of AbstractNearestSubscription.
         * @param facilityClient The client from which to retrieve facilities.
         * @param type The type of facility to search for.
         */
        constructor(facilityClient, type) {
            super();
            this.facilityClient = facilityClient;
            this.type = type;
            this.facilities = [];
            this.facilityIndex = new Map();
            this.startPromiseResolves = [];
            this.updatePromiseResolves = [];
            this.hasRequestedSession = false;
            this.searchInProgress = false;
        }
        /** @inheritdoc */
        get length() {
            return this.facilities.length;
        }
        /**
         * Whether or not this subscription has been started.
         * @returns True if started, false otherwise.
         */
        get started() {
            return this.session !== undefined;
        }
        /** @inheritdoc */
        getArray() {
            return this.facilities;
        }
        /** @inheritdoc */
        awaitStart() {
            if (this.session !== undefined) {
                return Promise.resolve();
            }
            return new Promise(resolve => { this.startPromiseResolves.push(resolve); });
        }
        /** @inheritdoc */
        start() {
            if (this.session !== undefined) {
                return Promise.resolve();
            }
            if (this.hasRequestedSession) {
                return this.awaitStart();
            }
            return new Promise(resolve => {
                this.hasRequestedSession = true;
                this.startPromiseResolves.push(resolve);
                this.startSearchSession().then(session => {
                    this.session = session;
                    this.startPromiseResolves.forEach(queuedResolve => { queuedResolve(); });
                    this.startPromiseResolves.length = 0;
                });
            });
        }
        /** @inheritdoc */
        update(lat, lon, radius, maxItems) {
            return new Promise(resolve => {
                this.updatePromiseResolves.push(resolve);
                if (this.searchInProgress) {
                    return;
                }
                this.doUpdate(lat, lon, radius, maxItems);
            });
        }
        /**
         * Executes an update of the nearest search subscription.
         * @param lat The latitude of the current search position.
         * @param lon The longitude of the current search position.
         * @param radius The radius of the search, in meters.
         * @param maxItems The maximum number of items to return in the search.
         */
        async doUpdate(lat, lon, radius, maxItems) {
            this.searchInProgress = true;
            if (!this.started) {
                await this.start();
            }
            const results = await this.session.searchNearest(lat, lon, radius, maxItems);
            await this.onResults(results);
            this.searchInProgress = false;
            this.updatePromiseResolves.forEach(resolve => { resolve(); });
            this.updatePromiseResolves.length = 0;
        }
        /**
         * Adds a facility to the collection.
         * @param facility The facility to add.
         * @param key The key to track this facility by.
         */
        addFacility(facility, key) {
            if (this.facilityIndex.has(key)) {
                console.warn(`Facility ${key} is already in the collection.`);
            }
            this.facilities.push(facility);
            this.facilityIndex.set(key, facility);
            this.notify(this.facilities.length - 1, exports.SubscribableArrayEventType.Added, facility);
        }
        /**
         * Removes a facility from the collection.
         * @param key The key of the facility to remove.
         */
        removeFacility(key) {
            const facility = this.facilityIndex.get(key);
            if (facility !== undefined) {
                const index = this.facilities.indexOf(facility);
                this.facilities.splice(index, 1);
                this.facilityIndex.delete(key);
                this.notify(index, exports.SubscribableArrayEventType.Removed, facility);
            }
        }
    }
    /**
     * A nearest search subscription for waypoint facilites, including logic for further filtering
     * of results beyond what the sim search API gives us.
     */
    class NearestWaypointSubscription extends AbstractNearestSubscription {
        /**
         * Creates a new instance of NearestWaypointSubscription.
         * @param facilityLoader An instance of the facility loader to search with.
         * @param type The type of facility to search for.
         * @param filterCb A function which filters results after they have been returned by this subscription's search
         * session. If not defined, no post-search session filtering will be performed.
         */
        constructor(facilityLoader, type, filterCb) {
            super(facilityLoader, type);
            this.facilityType = facilitySearchTypeMap.get(this.type);
            this.icaos = new Map();
            this.facilityCache = new Map();
            this.filterCb = filterCb;
        }
        /**
         * Sets this subscription's post-search session filter and refilters this subscription's latest results using the new
         * filter.
         * @param filter A function which filters results after they have been returned by this subscription's search
         * session, or `undefined` if no post-search session filtering is to be performed.
         */
        setFilterCb(filter) {
            this.filterCb = filter;
            this.refilter();
        }
        /**
         * Refilters the latest search results returned from this subscription's nearest search session.
         */
        refilter() {
            // Start the refresh of our data by iterating over the current entries and
            // removing any that no longer match the filter.
            if (this.filterCb) {
                for (const [uid, fac] of this.facilityIndex) {
                    if (!this.filterCb(fac)) {
                        this.removeFacility(uid);
                    }
                }
            }
            // Next go through our facility cache and add any existing entries that
            // hadn't previously matched but now do.
            for (const [uid, fac] of this.facilityCache) {
                if ((this.filterCb === undefined || this.filterCb(fac))) {
                    this.addFacility(fac, uid);
                }
            }
        }
        /** @inheritDoc */
        startSearchSession() {
            return this.facilityClient.startNearestSearchSessionWithIcaoStructs(this.type);
        }
        /** @inheritDoc */
        async onResults(results) {
            if (this.facilityType !== undefined) {
                const addedFacs = await this.getFacilities(results.added);
                for (let i = 0; i < results.removed.length; i++) {
                    const uid = ICAO.getUid(results.removed[i]);
                    this.icaos.delete(uid);
                    this.facilityCache.delete(uid);
                    this.removeFacility(uid);
                }
                for (let i = 0; i < results.added.length; i++) {
                    const icao = results.added[i];
                    const uid = ICAO.getUid(icao);
                    this.icaos.set(uid, icao);
                    const fac = addedFacs[i];
                    if (fac) {
                        this.facilityCache.set(uid, fac);
                        if (this.filterCb === undefined || this.filterCb(fac)) {
                            this.addFacility(fac, uid);
                        }
                    }
                }
            }
        }
        /**
         * Retrieves facilities for an array of ICAOs.
         * @param icaos The ICAOs for which to get facilities.
         * @returns A Promise which will be fulfilled with an array of facilities corresponding to the specified ICAOs. Each
         * position in the facilities array will contain either the facility for the ICAO at the corresponding position in
         * the ICAO array, or `null` if a facility for that ICAO could not be retrieved.
         */
        getFacilities(icaos) {
            if (this.facilityType === undefined) {
                return Promise.resolve([]);
            }
            return this.facilityClient.getFacilitiesOfType(this.facilityType, icaos);
        }
    }
    /**
     * A nearest search subscription for airport facilites.
     */
    class NearestAirportSubscription extends NearestWaypointSubscription {
        /**
         * Creates a new instance of NearestAirportSubscription.
         * @param facilityLoader The facility loader to use with this instance.
         * @param filterCb A function which filters results after they have been returned by this subscription's search
         * session. If not defined, no post-search session filtering will be performed.
         */
        constructor(facilityLoader, filterCb) {
            super(facilityLoader, exports.FacilitySearchType.Airport, filterCb);
            this.dataFlags = exports.AirportFacilityDataFlags.All;
            this.pendingDataFlags = this.dataFlags;
        }
        /**
         * Sets the airport search filter.
         * @param showClosed Whether or not to return closed airports in the search.
         * @param classMask A bitmask representing the classes of airports to show.
         */
        setFilter(showClosed, classMask) {
            if (this.session !== undefined) {
                this.session.setAirportFilter(showClosed, classMask);
            }
        }
        /**
         * Sets the extended airport filters for the airport nearest search.
         * @param surfaceTypeMask A bitmask of allowable runway surface types.
         * @param approachTypeMask A bitmask of allowable approach types.
         * @param toweredMask A bitmask of untowered (1) or towered (2) bits.
         * @param minRunwayLength The minimum allowable runway length, in meters.
         */
        setExtendedFilters(surfaceTypeMask, approachTypeMask, toweredMask, minRunwayLength) {
            if (this.session !== undefined) {
                this.session
                    .setExtendedAirportFilters(surfaceTypeMask, approachTypeMask, toweredMask, minRunwayLength);
            }
        }
        /**
         * Sets the bitflags describing the data to be loaded in the airport facilities provided by this subscription. All
         * airport facilities in this subscription's array are guaranteed to have *at least* as much loaded data as defined
         * by the bitflags. Changes to the bitflags will take effect the next time this subscription's search is updated.
         * @param flags The bitflags to set.
         */
        setDataFlags(flags) {
            this.pendingDataFlags = flags;
        }
        /** @inheritDoc */
        async onResults(results) {
            if (!BitFlags.isAll(this.dataFlags, this.pendingDataFlags)) {
                // The pending data flags are not a subset of the existing data flags. Therefore, we need to refresh all
                // facilities that are currently in the array and that are cached in order to ensure they have the required data.
                this.dataFlags = this.pendingDataFlags;
                for (let i = 0; i < results.removed.length; i++) {
                    this.icaos.delete(ICAO.getUid(results.removed[i]));
                }
                const icaos = Array.from(this.icaos.values());
                icaos.push(...results.added);
                const facs = await this.getFacilities(icaos);
                this.facilityCache.clear();
                this.facilityIndex.clear();
                this.facilities.length = 0;
                this.notify(0, exports.SubscribableArrayEventType.Cleared);
                for (let i = 0; i < icaos.length; i++) {
                    const icao = icaos[i];
                    const uid = ICAO.getUid(icao);
                    this.icaos.set(uid, icao);
                    const fac = facs[i];
                    if (fac) {
                        this.facilityCache.set(uid, fac);
                        if (this.filterCb === undefined || this.filterCb(fac)) {
                            this.addFacility(fac, uid);
                        }
                    }
                }
            }
            else {
                this.dataFlags = this.pendingDataFlags;
                super.onResults(results);
            }
        }
        /** @inheritDoc */
        getFacilities(icaos) {
            return this.facilityClient.getFacilities(icaos, this.dataFlags);
        }
    }
    /**
     * A nearest search subscription for intersection facilites.
     */
    class NearestIntersectionSubscription extends NearestWaypointSubscription {
        /**
         * Creates a new instance of NearestIntersectionSubscription.
         * @param facilityLoader The facility loader to use with this instance.
         * @param filterCb A function which filters results after they have been returned by this subscription's search
         * session. If not defined, no post-search session filtering will be performed.
         * @param filterDupTerminal Whether to filter out terminal intersections if their non-terminal counterparts are
         * also present in the subscription's results. Defaults to `false`.
         */
        constructor(facilityLoader, filterCb, filterDupTerminal = false) {
            super(facilityLoader, exports.FacilitySearchType.Intersection, filterCb);
            this.nonTerminalIcaoUidsToFilter = new Set();
            this.filterDupTerminal = filterDupTerminal;
        }
        /**
         * Sets the intersection search filter.
         * @param typeMask A bitmask representing the types of intersections to include in the search.
         * @param showTerminalWaypoints Whether to include terminal intersections in the search.
         */
        setFilter(typeMask, showTerminalWaypoints = true) {
            if (this.session !== undefined) {
                this.session.setIntersectionFilter(typeMask, showTerminalWaypoints);
            }
        }
        /**
         * Sets whether to filter out terminal intersections if their non-terminal counterparts are also present in this
         * subscription's results and refilters this subscription's latest results accordingly.
         * @param filter Whether to filter out terminal intersections if their non-terminal counterparts are also present in
         * this subscription's results.
         */
        setFilterDupTerminal(filter) {
            if (filter === this.filterDupTerminal) {
                return;
            }
            this.filterDupTerminal = filter;
            this.refilter();
        }
        /** @inheritdoc */
        refilter() {
            // Rebuild non-terminal key set
            this.nonTerminalIcaoUidsToFilter.clear();
            if (this.filterDupTerminal) {
                for (const [uid, fac] of this.facilityCache) {
                    if (ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.Intersection)
                        && !IntersectionFacilityUtils.isTerminal(fac.icaoStruct)
                        && (this.filterCb === undefined || this.filterCb(fac))) {
                        this.nonTerminalIcaoUidsToFilter.add(uid);
                    }
                }
            }
            // Start the refresh of our data by iterating over the current entries and
            // removing any that no longer match the filter.
            if (this.filterCb || this.filterDupTerminal) {
                for (const [uid, fac] of this.facilityIndex) {
                    if ((this.filterCb && !this.filterCb(fac))
                        || (this.filterDupTerminal
                            && ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.Intersection)
                            && IntersectionFacilityUtils.isTerminal(fac.icaoStruct)
                            && this.nonTerminalIcaoUidsToFilter.has(ICAO.getUid(IntersectionFacilityUtils.getNonTerminalIcaoValue(fac.icaoStruct))))) {
                        this.removeFacility(uid);
                    }
                }
            }
            // Next go through our facility cache and add any existing entries that
            // hadn't previously matched but now do.
            for (const [uid, fac] of this.facilityCache) {
                if (!this.facilityIndex.get(uid)) {
                    if ((this.filterCb === undefined || this.filterCb(fac))
                        && (!this.filterDupTerminal
                            || !ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.Intersection)
                            || !IntersectionFacilityUtils.isTerminal(fac.icaoStruct)
                            || !this.nonTerminalIcaoUidsToFilter.has(ICAO.getUid(IntersectionFacilityUtils.getNonTerminalIcaoValue(fac.icaoStruct))))) {
                        this.addFacility(fac, uid);
                    }
                }
            }
        }
        /** @inheritDoc */
        async onResults(results) {
            const facilityType = facilitySearchTypeMap.get(this.type);
            if (facilityType !== undefined) {
                const added = (await this.facilityClient.getFacilities(results.added));
                for (let i = 0; i < results.removed.length; i++) {
                    const uid = ICAO.getUid(results.removed[i]);
                    this.nonTerminalIcaoUidsToFilter.delete(uid);
                    this.facilityCache.delete(uid);
                    this.removeFacility(uid);
                }
                for (let i = 0; i < added.length; i++) {
                    const fac = added[i];
                    if (fac) {
                        const uid = ICAO.getUid(fac.icaoStruct);
                        this.facilityCache.set(uid, fac);
                        if (this.filterDupTerminal
                            && ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.Intersection)
                            && !IntersectionFacilityUtils.isTerminal(fac)
                            && (this.filterCb === undefined || this.filterCb(fac))) {
                            this.nonTerminalIcaoUidsToFilter.add(uid);
                        }
                    }
                }
                for (let i = 0; i < added.length; i++) {
                    const fac = added[i];
                    if (fac) {
                        const uid = ICAO.getUid(fac.icaoStruct);
                        if ((this.filterCb === undefined || this.filterCb(fac))
                            && (!this.filterDupTerminal
                                || !ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.Intersection)
                                || !IntersectionFacilityUtils.isTerminal(fac)
                                || !this.nonTerminalIcaoUidsToFilter.has(ICAO.getUid(IntersectionFacilityUtils.getNonTerminalIcaoValue(fac.icaoStruct))))) {
                            this.addFacility(fac, uid);
                        }
                    }
                }
            }
        }
    }
    /**
     * A nearest search subscription for VOR facilites.
     */
    class NearestVorSubscription extends NearestWaypointSubscription {
        /**
         * Creates a new instance of NearestVorSubscription.
         * @param facilityLoader The facility loader to use with this instance.
         */
        constructor(facilityLoader) {
            super(facilityLoader, exports.FacilitySearchType.Vor);
        }
        /**
         * Sets the VOR search filter.
         * @param classMask A bitmask to determine which JS VOR classes to show.
         * @param typeMask A bitmask to determine which JS VOR types to show.
         */
        setVorFilter(classMask, typeMask) {
            if (this.session !== undefined) {
                this.session.setVorFilter(classMask, typeMask);
            }
        }
    }
    /**
     * A nearest search subscription for NDB facilites.
     */
    class NearestNdbSubscription extends NearestWaypointSubscription {
        /**
         * Creates a new instance of NearestNdbSubscription.
         * @param facilityLoader The facility loader to use with this instance.
         */
        constructor(facilityLoader) {
            super(facilityLoader, exports.FacilitySearchType.Ndb);
        }
    }
    /**
     * A nearest search subscription for USR facilites.
     */
    class NearestUsrSubscription extends NearestWaypointSubscription {
        /**
         * Creates a new NearestUsrSubscription.
         * @param facilityLoader The facility loader to use with this instance.
         */
        constructor(facilityLoader) {
            super(facilityLoader, exports.FacilitySearchType.User);
        }
        /**
         * Sets the facility search filter.
         * @param filter A function to filter the search results.
         */
        setFacilityFilter(filter) {
            if (this.session !== undefined) {
                this.session.setFacilityFilter(filter);
            }
        }
    }
    /**
     * A wrapper for a {@link NearestSearchSession} that automatically adjusts the number of
     * search results requested from the sim to minimize search load while still attempting to
     * provide the total number of results needed by the user.
     */
    class AdaptiveNearestSubscription extends AbstractSubscribableArray {
        /**
         * Creates an instance of AdaptiveNearestSubscription.
         * @param innerSubscription A {@link NearestSubscription} to use as our inner search.
         * @param absoluteMaxItems The maximum number of results to request in any search.
         */
        constructor(innerSubscription, absoluteMaxItems) {
            super();
            this.innerSubscription = innerSubscription;
            this.sortFunc = (a, b) => this.pos.distance(a) - this.pos.distance(b);
            /** The array that holds the results of our latest search. */
            this.facilities = [];
            /** The number of items we are requesting from the inner search to meet current demands. */
            this.derivedMaxItems = 0;
            /** Whether we have a search in progress already. */
            this.searchInProgress = false;
            /** A reusable GeoPoint for sorting by distance. */
            this.pos = new GeoPoint(0, 0);
            this.diffMap = new Map();
            this.updatePromiseResolves = [];
            this.absoluteMaxItems = SubscribableUtils.toSubscribable(absoluteMaxItems, true);
        }
        /** @inheritdoc */
        get length() {
            return this.facilities.length;
        }
        /** @inheritdoc */
        getArray() {
            return this.facilities;
        }
        /** @inheritdoc */
        get started() {
            return this.innerSubscription.started;
        }
        /** @inheritdoc */
        awaitStart() {
            return this.innerSubscription.awaitStart();
        }
        /** @inheritdoc */
        start() {
            return this.innerSubscription.start();
        }
        /** @inheritdoc */
        update(lat, lon, radius, maxItems) {
            return new Promise(resolve => {
                this.updatePromiseResolves.push(resolve);
                if (this.searchInProgress) {
                    return;
                }
                this.doUpdate(lat, lon, radius, maxItems);
            });
        }
        /**
         * Executes an update of the nearest search subscription.
         * @param lat The latitude of the current search position.
         * @param lon The longitude of the current search position.
         * @param radius The radius of the search, in meters.
         * @param maxItems The maximum number of items to return in the search.
         */
        async doUpdate(lat, lon, radius, maxItems) {
            this.searchInProgress = true;
            this.pos.set(lat, lon);
            maxItems = Math.max(0, maxItems);
            if (maxItems > this.derivedMaxItems) {
                this.derivedMaxItems = maxItems;
            }
            // When the subscription updates, any changes from airports added or removed cause
            // onSourceChanged below to trigger.   That will update our facilites store, because
            // it means the airport is no longer in the raw search data.
            await this.innerSubscription.update(lat, lon, radius, this.derivedMaxItems);
            if (this.innerSubscription.length > maxItems) {
                // We have more returned facilities in our search than the user has asked for.
                // Begin a ramp-down of our search size. Ramp down is less aggressive than
                // ramp up to avoid flapping between the two states.
                this.derivedMaxItems = Math.max(Math.round(this.derivedMaxItems - (this.derivedMaxItems * AdaptiveNearestSubscription.RAMP_DOWN_FACTOR)), maxItems);
            }
            else {
                // We have either exactly enough or too few facilities.  If we have too few, ramp
                // up our search size until we either have enough or hit the maximum allowed search
                // quantity.
                const absoluteMaxItems = this.absoluteMaxItems.get();
                while (this.innerSubscription.length < maxItems && this.derivedMaxItems < absoluteMaxItems) {
                    this.derivedMaxItems = Math.min(Math.round(this.derivedMaxItems * AdaptiveNearestSubscription.RAMP_UP_FACTOR), absoluteMaxItems);
                    await this.innerSubscription.update(lat, lon, radius, this.derivedMaxItems);
                }
            }
            if (this.innerSubscription.length > maxItems) {
                if (maxItems > 1) {
                    // Filter out the farthest results until we have exactly as many results as the user has asked for.
                    const sorted = Array.from(this.innerSubscription.getArray()).sort(this.sortFunc);
                    sorted.length = maxItems;
                    this.diffAndNotify(sorted);
                }
                else if (maxItems === 1) {
                    this.diffAndNotify([this.findNearest(this.innerSubscription.getArray())]);
                }
                else {
                    this.diffAndNotify(AdaptiveNearestSubscription.EMPTY_ARRAY);
                }
            }
            else {
                this.diffAndNotify(this.innerSubscription.getArray());
            }
            this.searchInProgress = false;
            this.updatePromiseResolves.forEach(resolve => { resolve(); });
            this.updatePromiseResolves.length = 0;
        }
        /**
         * Finds the nearest facility in an array.
         * @param array A non-empty array of facilities.
         * @returns The nearest facility in the specified array.
         */
        findNearest(array) {
            let nearest = array[0];
            let nearestDistance = this.pos.distance(nearest);
            for (let i = 1; i < array.length; i++) {
                const fac = array[i];
                const distance = this.pos.distance(fac);
                if (distance < nearestDistance) {
                    nearest = fac;
                    nearestDistance = distance;
                }
            }
            return nearest;
        }
        /**
         * Diffs a new facility array against this subscription's current facility array, makes the necessary changes to
         * the current facility array so that it contains the same facilities as the new one, and notifies subscribers of the
         * changes.
         * @param newArray A new facility array.
         */
        diffAndNotify(newArray) {
            if (this.facilities.length === 0 && newArray.length === 0) {
                // Old and new arrays are both empty. Nothing to do.
                return;
            }
            if (newArray.length === 0) {
                // New array is empty. Clear the old array.
                this.facilities.length = 0;
                this.notify(0, exports.SubscribableArrayEventType.Cleared);
                return;
            }
            if (this.facilities.length === 0) {
                // Old array is empty. Add every item from the new array in order.
                for (let i = 0; i < newArray.length; i++) {
                    this.facilities[i] = newArray[i];
                }
                this.facilities.length = newArray.length;
                this.notify(0, exports.SubscribableArrayEventType.Added, this.facilities);
                return;
            }
            // Remove every item from the old array that is not in the new array.
            for (let i = 0; i < newArray.length; i++) {
                this.diffMap.set(newArray[i].icao, newArray[i]);
            }
            for (let i = this.facilities.length - 1; i >= 0; i--) {
                const old = this.facilities[i];
                if (this.diffMap.has(old.icao)) {
                    this.diffMap.delete(old.icao);
                }
                else {
                    this.facilities.splice(i, 1);
                    this.notify(i, exports.SubscribableArrayEventType.Removed, old);
                }
            }
            // Add every item from the new array that is not in the old array (these items are now contained in diffMap).
            for (const toAdd of this.diffMap.values()) {
                this.facilities.push(toAdd);
                this.notify(this.facilities.length - 1, exports.SubscribableArrayEventType.Added, toAdd);
            }
            this.diffMap.clear();
        }
    }
    AdaptiveNearestSubscription.RAMP_UP_FACTOR = 1.33;
    AdaptiveNearestSubscription.RAMP_DOWN_FACTOR = 0.1;
    AdaptiveNearestSubscription.EMPTY_ARRAY = [];

    /**
     * A singleton context of all nearest facility information.
     */
    class NearestContext {
        /**
         * Gets an instance of the NearestContext.
         * @returns An instance of the NearestContext.
         * @throws An error if the NearestContext has not yet been initailized.
         */
        static getInstance() {
            if (this.instance !== undefined) {
                return this.instance;
            }
            throw new Error('NearestContext was not initialized.');
        }
        /**
         * Initialized the NearestContext instance.
         * @param facilityLoader The facility loader to use for the instance.
         * @param bus The EventBus to use with this instance.
         * @param planePos A subscribable which provides the current position of the airplane. If not defined, the context
         * will automatically retrieve the position from the event bus.
         * @throws An error if the NearestContext is already initialized.
         */
        static initialize(facilityLoader, bus, planePos) {
            if (this.instance === undefined) {
                this.instance = new NearestContext(facilityLoader, bus, planePos);
                this.initializedSubEvent.notify(null, this.instance);
            }
            else {
                throw new Error('NearestContext was already initialized.');
            }
        }
        /**
         * Subscribes to this NearestContext being initialized, or invokes the handler immediately if it is already initialized
         *
         * @param handler the handler
         *
         * @returns the subscription, if the action was not immediately performed
         */
        static onInitialized(handler) {
            if (this.instance) {
                handler(this.instance);
                return null;
            }
            else {
                return this.initializedSubEvent.on((_, instance) => handler(instance));
            }
        }
        /**
         * Whether the NearestContext is initialized.
         * @returns true if initialized.
         */
        static get isInitialized() {
            return NearestContext.instance !== undefined;
        }
        /**
         * Creates an instance of a NearestContext.
         * @param facilityLoader The facility loader to use for this instance.
         * @param bus An instance of the EventBus.
         * @param planePos A subscribable which provides the current position of the airplane. If not defined, the context
         * will automatically retrieve the position from the event bus.
         */
        constructor(facilityLoader, bus, planePos) {
            this.facilityLoader = facilityLoader;
            this.bus = bus;
            /** The max number of airports in the context.*/
            this.maxAirports = 25;
            /** The max number of VORs in the context. */
            this.maxVors = 25;
            /** The max number of intersections in the context. */
            this.maxIntersections = 25;
            /** The max number of NDBs in the context. */
            this.maxNdbs = 25;
            /** The max number of user facilities in the context. */
            this.maxUsrs = 25;
            /** The search radius for airports, in nautical miles. */
            this.airportRadius = 50;
            /** The search radius for VORs, in nautical miles. */
            this.vorRadius = 150;
            /** The search radius for intersections, in nautical miles. */
            this.intersectionRadius = 10;
            /** The search radius for NDBs, in nautical miles. */
            this.ndbRadius = 150;
            /** The search radius for user facilities, in nautical miles */
            this.usrRadius = 150;
            this.position = new GeoPoint(0, 0);
            this.airports = new NearestAirportSubscription(facilityLoader);
            this.vors = new NearestVorSubscription(facilityLoader);
            this.intersections = new NearestIntersectionSubscription(facilityLoader);
            this.ndbs = new NearestNdbSubscription(facilityLoader);
            this.usrs = new NearestUsrSubscription(facilityLoader);
            if (planePos) {
                planePos.sub(pos => this.position.set(pos), true);
            }
            else {
                this.bus.getSubscriber().on('gps-position')
                    .handle(pos => this.position.set(pos.lat, pos.long));
            }
            this.airports.start();
            this.vors.start();
            this.intersections.start();
            this.ndbs.start();
            this.usrs.start();
        }
        /**
         * Updates the NearestContext.
         */
        async update() {
            await Promise.all([
                this.airports.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.airportRadius, UnitType.METER), this.maxAirports),
                this.intersections.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.intersectionRadius, UnitType.METER), this.maxIntersections),
                this.vors.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.vorRadius, UnitType.METER), this.maxVors),
                this.ndbs.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.ndbRadius, UnitType.METER), this.maxNdbs),
                this.usrs.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.usrRadius, UnitType.METER), this.maxUsrs),
            ]);
        }
        /**
         * Get the local ICAO region code based on nearby facility data.
         * @returns The region code.
         */
        getRegionCode() {
            const nearest = Array.from(this.airports.getArray()).sort(this.orderByPPosDistance.bind(this));
            for (let i = 0; i < nearest.length; i++) {
                const region = AirportUtils.tryGetRegionCode(nearest[i]);
                if (region !== undefined) {
                    return region;
                }
            }
            return undefined;
        }
        /**
         * Get the variant of the region code used in airport idents.   Generally this will
         * be the region code except for the case of the US, where it will just be 'K'.
         * @returns The region code or just 'K' for the US.
         */
        getRegionIdent() {
            const region = this.getRegionCode();
            if (region !== undefined) {
                return region[0] === 'K' ? 'K' : region;
            }
            return undefined;
        }
        /**
         * Gets the first letter of the local ICAO region code.
         * @returns The airport region letter.
         */
        getRegionLetter() {
            const region = this.getRegionCode();
            return region !== undefined ? region[0] : undefined;
        }
        /**
         * Gets the nearest facility for a given type.
         * @param facilityType The type of facility.
         * @returns The nearest facility for a given type.
         */
        getNearest(facilityType) {
            switch (facilityType) {
                case exports.FacilityType.Airport:
                    return this.findNearest(this.airports.getArray());
                case exports.FacilityType.Intersection:
                    return this.findNearest(this.intersections.getArray());
                case exports.FacilityType.VOR:
                    return this.findNearest(this.vors.getArray());
                case exports.FacilityType.NDB:
                    return this.findNearest(this.ndbs.getArray());
                case exports.FacilityType.USR:
                    return this.findNearest(this.usrs.getArray());
                default:
                    return undefined;
            }
        }
        /**
         * Finds the nearest facility in an array.
         * @param array A non-empty array of facilities.
         * @returns The nearest facility in the specified array.
         */
        findNearest(array) {
            let nearest = undefined;
            let nearestDistance = Infinity;
            for (let i = 0; i < array.length; i++) {
                const fac = array[i];
                const distance = this.position.distance(fac);
                if (distance < nearestDistance) {
                    nearest = fac;
                    nearestDistance = distance;
                }
            }
            return nearest;
        }
        /**
         * Orders facilities by their distance to the plane PPOS.
         * @param a The first facility.
         * @param b The second facility.
         * @returns The comparison order number.
         */
        orderByPPosDistance(a, b) {
            const aDist = this.position.distance(a.lat, a.lon);
            const bDist = this.position.distance(b.lat, b.lon);
            if (aDist < bDist) {
                return -1;
            }
            if (aDist > bDist) {
                return 1;
            }
            return 0;
        }
    }
    NearestContext.initializedSubEvent = new SubEvent();
    /**
     * A singleton context of all nearest facility information which uses adaptive searches.
     */
    class AdaptiveNearestContext {
        // eslint-disable-next-line jsdoc/require-returns
        /** The maximum number of airports returned by this context's inner searches. */
        get maxAirportsAbsolute() {
            return this._maxAirportsAbsolute.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set maxAirportsAbsolute(val) {
            this._maxAirportsAbsolute.set(val);
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The maximum number of VORs returned by this context's inner searches. */
        get maxVorsAbsolute() {
            return this._maxVorsAbsolute.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set maxVorsAbsolute(val) {
            this._maxVorsAbsolute.set(val);
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The maximum number of intersections returned by this context's inner searches. */
        get maxIntersectionsAbsolute() {
            return this._maxIntersectionsAbsolute.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set maxIntersectionsAbsolute(val) {
            this._maxIntersectionsAbsolute.set(val);
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The maximum number of NDBs returned by this context's inner searches. */
        get maxNdbsAbsolute() {
            return this._maxNdbsAbsolute.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set maxNdbsAbsolute(val) {
            this._maxNdbsAbsolute.set(val);
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The maximum number of user facilities returned by this context's inner searches. */
        get maxUsrsAbsolute() {
            return this._maxUsrsAbsolute.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set maxUsrsAbsolute(val) {
            this._maxUsrsAbsolute.set(val);
        }
        /**
         * Gets an instance of the AdaptiveNearestContext.
         * @returns An instance of the AdaptiveNearestContext.
         * @throws An error if the AdaptiveNearestContext has not yet been initailized.
         */
        static getInstance() {
            if (this.instance !== undefined) {
                return this.instance;
            }
            throw new Error('AdaptiveNearestContext was not initialized.');
        }
        /**
         * Initialized the AdaptiveNearestContext instance.
         * @param facilityLoader The facility loader to use for the instance.
         * @param bus The EventBus to use with this instance.
         * @param planePos A subscribable which provides the current position of the airplane. If not defined, the context
         * will automatically retrieve the position from the event bus.
         * @throws An error if the AdaptiveNearestContext is already initialized.
         */
        static initialize(facilityLoader, bus, planePos) {
            if (this.instance === undefined) {
                this.instance = new AdaptiveNearestContext(facilityLoader, bus, planePos);
                this.initializedSubEvent.notify(null, this.instance);
            }
            else {
                throw new Error('AdaptiveNearestContext was already initialized.');
            }
        }
        /**
         * Subscribes to this NearestContext being initialized, or invokes the handler immediately if it is already initialized
         *
         * @param handler the handler
         *
         * @returns the subscription, if the action was not immediately performed
         */
        static onInitialized(handler) {
            if (this.instance) {
                handler(this.instance);
                return null;
            }
            else {
                return this.initializedSubEvent.on((_, instance) => handler(instance));
            }
        }
        /**
         * Creates an instance of a AdaptiveNearestContext.
         * @param facilityLoader The facility loader to use for this instance.
         * @param bus An instance of the EventBus.
         * @param planePos A subscribable which provides the current position of the airplane. If not defined, the context
         * will automatically retrieve the position from the event bus.
         */
        constructor(facilityLoader, bus, planePos) {
            this.facilityLoader = facilityLoader;
            this.bus = bus;
            /** The maximum number of airports in the context. */
            this.maxAirports = 25;
            /** The maximum number of VORs in the context. */
            this.maxVors = 25;
            /** The maximum number of intersections in the context. */
            this.maxIntersections = 25;
            /** The maximum number of NDBs in the context. */
            this.maxNdbs = 25;
            /** The maximum number of user facilities in the context. */
            this.maxUsrs = 25;
            /** The search radius for airports, in nautical miles. */
            this.airportRadius = 50;
            /** The search radius for VORs, in nautical miles. */
            this.vorRadius = 150;
            /** The search radius for intersections, in nautical miles. */
            this.intersectionRadius = 10;
            /** The search radius for NDBs, in nautical miles. */
            this.ndbRadius = 150;
            /** The search radius for user facilities, in nautical miles. */
            this.usrRadius = 150;
            this.position = new GeoPoint(0, 0);
            this._maxAirportsAbsolute = Subject.create(100);
            this._maxVorsAbsolute = Subject.create(100);
            this._maxIntersectionsAbsolute = Subject.create(100);
            this._maxNdbsAbsolute = Subject.create(100);
            this._maxUsrsAbsolute = Subject.create(100);
            this.airports = new AdaptiveNearestSubscription(new NearestAirportSubscription(facilityLoader), this._maxAirportsAbsolute);
            this.vors = new AdaptiveNearestSubscription(new NearestVorSubscription(facilityLoader), this._maxVorsAbsolute);
            this.intersections = new AdaptiveNearestSubscription(new NearestIntersectionSubscription(facilityLoader), this._maxIntersectionsAbsolute);
            this.ndbs = new AdaptiveNearestSubscription(new NearestNdbSubscription(facilityLoader), this._maxNdbsAbsolute);
            this.usrs = new AdaptiveNearestSubscription(new NearestUsrSubscription(facilityLoader), this._maxUsrsAbsolute);
            if (planePos) {
                planePos.sub(pos => this.position.set(pos), true);
            }
            else {
                this.bus.getSubscriber().on('gps-position')
                    .handle(pos => this.position.set(pos.lat, pos.long));
            }
            this.airports.start();
            this.vors.start();
            this.intersections.start();
            this.ndbs.start();
            this.usrs.start();
        }
        /**
         * Updates this context.
         */
        async update() {
            await Promise.all([
                this.airports.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.airportRadius, UnitType.METER), this.maxAirports),
                this.intersections.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.intersectionRadius, UnitType.METER), this.maxIntersections),
                this.vors.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.vorRadius, UnitType.METER), this.maxVors),
                this.ndbs.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.ndbRadius, UnitType.METER), this.maxNdbs),
                this.usrs.update(this.position.lat, this.position.lon, UnitType.NMILE.convertTo(this.usrRadius, UnitType.METER), this.maxUsrs),
            ]);
        }
        /**
         * Gets the airport region letter to use for the first character in waypoint inputs.
         * @returns The airport region letter.
         */
        getRegionLetter() {
            const nearest = Array.from(this.airports.getArray()).sort(this.orderByPPosDistance.bind(this));
            for (let i = 0; i < nearest.length; i++) {
                const region = AirportUtils.tryGetRegionCode(nearest[i]);
                if (region !== undefined) {
                    return region[0];
                }
            }
            return undefined;
        }
        /**
         * Gets the nearest facility for a given type.
         * @param facilityType The type of facility.
         * @returns The nearest facility for a given type.
         */
        getNearest(facilityType) {
            switch (facilityType) {
                case exports.FacilityType.Airport:
                    return this.findNearest(this.airports.getArray());
                case exports.FacilityType.Intersection:
                    return this.findNearest(this.intersections.getArray());
                case exports.FacilityType.VOR:
                    return this.findNearest(this.vors.getArray());
                case exports.FacilityType.NDB:
                    return this.findNearest(this.ndbs.getArray());
                case exports.FacilityType.USR:
                    return this.findNearest(this.usrs.getArray());
                default:
                    return undefined;
            }
        }
        /**
         * Finds the nearest facility in an array.
         * @param array A non-empty array of facilities.
         * @returns The nearest facility in the specified array.
         */
        findNearest(array) {
            let nearest = undefined;
            let nearestDistance = Infinity;
            for (let i = 0; i < array.length; i++) {
                const fac = array[i];
                const distance = this.position.distance(fac);
                if (distance < nearestDistance) {
                    nearest = fac;
                    nearestDistance = distance;
                }
            }
            return nearest;
        }
        /**
         * Orders facilities by their distance to the plane PPOS.
         * @param a The first facility.
         * @param b The second facility.
         * @returns The comparison order number.
         */
        orderByPPosDistance(a, b) {
            const aDist = this.position.distance(a.lat, a.lon);
            const bDist = this.position.distance(b.lat, b.lon);
            if (aDist < bDist) {
                return -1;
            }
            if (aDist > bDist) {
                return 1;
            }
            return 0;
        }
    }
    AdaptiveNearestContext.initializedSubEvent = new SubEvent();

    /**
     * A task queue backed by an array.
     */
    class ArrayTaskQueue {
        /**
         * Constructor.
         * @param tasks The array of tasks in this queue.
         */
        constructor(tasks) {
            this.tasks = tasks;
            this.head = 0;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        hasNext() {
            return this.head < this.tasks.length;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        next() {
            return this.tasks[this.head++];
        }
    }

    /**
     * A process which dispatches tasks in a task queue potentially over multiple frames.
     */
    class ThrottledTaskQueueProcess {
        /**
         * Constructor.
         * @param queue The queue to process.
         * @param handler A handler which defines the behavior of this process.
         */
        constructor(queue, handler) {
            this.queue = queue;
            this.handler = handler;
            this._hasStarted = false;
            this._hasEnded = false;
            this._shouldAbort = false;
        }
        /**
         * Checks whether this process has been started.
         * @returns whether this process has been started.
         */
        hasStarted() {
            return this._hasStarted;
        }
        /**
         * Checks whether this process has ended.
         * @returns whether this process has ended.
         */
        hasEnded() {
            return this._hasEnded;
        }
        /**
         * Starts this process.
         */
        start() {
            this._hasStarted = true;
            this.processQueue(0);
        }
        /**
         * Processes the queue.
         * @param elapsedFrameCount The number of frames elapsed since queue processing started.
         */
        processQueue(elapsedFrameCount) {
            let dispatchCount = 0;
            const t0 = performance.now();
            while (!this._shouldAbort && this.queue.hasNext()) {
                if (this.handler.canContinue(elapsedFrameCount, dispatchCount, performance.now() - t0)) {
                    const task = this.queue.next();
                    task();
                    dispatchCount++;
                }
                else {
                    break;
                }
            }
            if (this._shouldAbort) {
                return;
            }
            if (!this.queue.hasNext()) {
                this.handler.onFinished(elapsedFrameCount);
                this._hasEnded = true;
            }
            else {
                this.handler.onPaused(elapsedFrameCount);
                requestAnimationFrame(this.processQueue.bind(this, elapsedFrameCount + 1));
            }
        }
        /**
         * Aborts this process. Has no effect if the process has not been started or if it has already ended.
         */
        abort() {
            if (this._hasStarted && !this._hasEnded) {
                this.handler.onAborted();
                this._shouldAbort = true;
                this._hasEnded = true;
            }
        }
    }

    /**
     * A nearest search session for boundaries (airspaces) in the form of LodBoundary objects.
     */
    class NearestLodBoundarySearchSession {
        /**
         * Constructor.
         * @param cache The boundary cache this search session uses.
         * @param session The nearest boundary facility search session this search session uses.
         * @param frameBudget The maximum amount of time allotted per frame to retrieve and process LodBoundary objects, in
         * milliseconds.
         */
        constructor(cache, session, frameBudget) {
            this.cache = cache;
            this.session = session;
            this.frameBudget = frameBudget;
        }
        /**
         * Searches for the nearest boundaries around a specified location.
         * @param lat The latitude of the search center, in degrees.
         * @param lon The longitude of the search center, in degrees.
         * @param radius The radius of the search, in meters.
         * @param maxItems The maximum number of items for which to search.
         * @returns The nearest search results.
         */
        async searchNearest(lat, lon, radius, maxItems) {
            const facilityResults = await this.session.searchNearest(lat, lon, radius, maxItems);
            const results = { added: [], removed: facilityResults.removed };
            const tasks = facilityResults.added.map((fac, index) => () => { results.added[index] = this.cache.get(fac); });
            await new Promise(resolve => {
                const taskQueue = new ThrottledTaskQueueProcess(new ArrayTaskQueue(tasks), new NearestLodBoundarySearchTaskQueueHandler(this.frameBudget, resolve));
                taskQueue.start();
            });
            return results;
        }
        /**
         * Sets this session's boundary class filter. The new filter takes effect with the next search executed in this
         * session.
         * @param classMask A bitmask defining the boundary classes to include in the search (`0`: exclude, `1`: include).
         * The bit index for each boundary class is equal to the value of the corresponding `BoundaryType` enum.
         */
        setFilter(classMask) {
            this.session.setBoundaryFilter(classMask);
        }
    }
    /**
     * A throttled task queue handler for retrieving and creating new LodBoundary objects in response to a nearest search.
     */
    class NearestLodBoundarySearchTaskQueueHandler {
        /**
         * Constructor.
         * @param frameBudget The maximum amount of time allotted per frame to retrieve and process LodBoundary objects, in
         * milliseconds.
         * @param resolve The Promise resolve function this handler will call when the task queue is finished.
         */
        constructor(frameBudget, resolve) {
            this.frameBudget = frameBudget;
            this.resolve = resolve;
        }
        /** @inheritdoc */
        onStarted() {
            // noop
        }
        /** @inheritdoc */
        canContinue(elapsedFrameCount, dispatchedTaskCount, timeElapsed) {
            return timeElapsed < this.frameBudget;
        }
        /** @inheritdoc */
        onPaused() {
            // noop
        }
        /** @inheritdoc */
        onFinished() {
            this.resolve();
        }
        /** @inheritdoc */
        onAborted() {
            // noop
        }
    }

    /**
     * Implementation of MapCachedCanvasLayerReference.
     */
    class MapCachedCanvasLayerReferenceClass {
        constructor() {
            this._center = new GeoPoint(0, 0);
            this._scaleFactor = 1;
            this._rotation = 0;
        }
        /** @inheritdoc */
        get center() {
            return this._center.readonly;
        }
        /** @inheritdoc */
        get scaleFactor() {
            return this._scaleFactor;
        }
        /** @inheritdoc */
        get rotation() {
            return this._rotation;
        }
        /**
         * Syncs this reference with the current state of a map projection.
         * @param mapProjection The map projection with which to sync.
         */
        syncWithMapProjection(mapProjection) {
            this._center.set(mapProjection.getCenter());
            this._scaleFactor = mapProjection.getScaleFactor();
            this._rotation = mapProjection.getRotation();
        }
        /**
         * Syncs this reference with another reference.
         * @param reference - the reference with which to sync.
         */
        syncWithReference(reference) {
            this._center.set(reference.center);
            this._scaleFactor = reference.scaleFactor;
            this._rotation = reference.rotation;
        }
    }
    /**
     * Implementation of MapCachedCanvasLayerTransform.
     */
    class MapCachedCanvasLayerTransformClass {
        constructor() {
            this._scale = 0;
            this._rotation = 0;
            this._translation = new Float64Array(2);
            this._margin = 0;
            this._marginRemaining = 0;
        }
        /** @inheritdoc */
        get scale() {
            return this._scale;
        }
        /** @inheritdoc */
        get rotation() {
            return this._rotation;
        }
        /** @inheritdoc */
        get translation() {
            return this._translation;
        }
        /** @inheritdoc */
        get margin() {
            return this._margin;
        }
        /** @inheritdoc */
        get marginRemaining() {
            return this._marginRemaining;
        }
        /**
         * Updates this transform given the current map projection and a reference.
         * @param mapProjection The current map projection.
         * @param reference The reference to use.
         * @param referenceMargin The reference margin, in pixels.
         */
        update(mapProjection, reference, referenceMargin) {
            this._scale = mapProjection.getScaleFactor() / reference.scaleFactor;
            this._rotation = mapProjection.getRotation() - reference.rotation;
            mapProjection.project(reference.center, this._translation);
            Vec2Math.sub(this._translation, mapProjection.getCenterProjected(), this._translation);
            this._margin = referenceMargin * this._scale;
            this._marginRemaining = this._margin - Math.max(Math.abs(this._translation[0]), Math.abs(this._translation[1]));
        }
        /**
         * Copies another transform's parameters to this one.
         * @param other The other transform.
         */
        copyFrom(other) {
            this._scale = other.scale;
            this._rotation = other.rotation;
            this._translation.set(other.translation);
            this._margin = other.margin;
        }
    }
    /**
     * An implementation of MapCachedCanvasLayerCanvasInstance.
     */
    class MapCachedCanvasLayerCanvasInstanceClass extends MapCanvasLayerCanvasInstanceClass {
        /**
         * Creates a new canvas instance.
         * @param canvas The canvas element.
         * @param context The canvas 2D rendering context.
         * @param isDisplayed Whether the canvas is displayed.
         * @param getReferenceMargin A function which gets this canvas instance's reference margin, in pixels. The reference
         * margin is the maximum amount of translation allowed without invalidation at a scale factor of 1.
         */
        constructor(canvas, context, isDisplayed, getReferenceMargin) {
            super(canvas, context, isDisplayed);
            this.getReferenceMargin = getReferenceMargin;
            this._reference = new MapCachedCanvasLayerReferenceClass();
            this._transform = new MapCachedCanvasLayerTransformClass();
            this._isInvalid = false;
            this._geoProjection = new MercatorProjection();
            this.canvasTransform = CssTransformSubject.create(CssTransformBuilder.concat(CssTransformBuilder.scale(), CssTransformBuilder.translate('px'), CssTransformBuilder.rotate('rad')));
            this.canvasTransform.sub(transform => { this.canvas.style.transform = transform; }, true);
        }
        /** @inheritdoc */
        get reference() {
            return this._reference;
        }
        /** @inheritdoc */
        get transform() {
            return this._transform;
        }
        /** @inheritdoc */
        get isInvalid() {
            return this._isInvalid;
        }
        /** @inheritdoc */
        get geoProjection() {
            return this._geoProjection;
        }
        /** @inheritdoc */
        syncWithMapProjection(mapProjection) {
            const projectedCenter = Vec2Math.set(this.canvas.width / 2, this.canvas.height / 2, MapCachedCanvasLayerCanvasInstanceClass.tempVec2_1);
            this._reference.syncWithMapProjection(mapProjection);
            this._geoProjection.copyParametersFrom(mapProjection.getGeoProjection()).setTranslation(projectedCenter);
            this._transform.update(mapProjection, this.reference, this.getReferenceMargin());
            this._isInvalid = false;
            if (this.isDisplayed) {
                this.transformCanvasElement();
            }
        }
        /** @inheritdoc */
        syncWithCanvasInstance(other) {
            this._reference.syncWithReference(other.reference);
            this._geoProjection.copyParametersFrom(other.geoProjection);
            this._transform.copyFrom(other.transform);
            this._isInvalid = other.isInvalid;
            if (this.isDisplayed && !this._isInvalid) {
                this.transformCanvasElement();
            }
        }
        /**
         * Updates this canvas instance's transform given the current map projection.
         * @param mapProjection The current map projection.
         */
        updateTransform(mapProjection) {
            this._transform.update(mapProjection, this.reference, this.getReferenceMargin());
            if (!this._isInvalid) {
                const scaleFactorRatio = mapProjection.getScaleFactor() / this._reference.scaleFactor;
                this._isInvalid = scaleFactorRatio >= MapCachedCanvasLayerCanvasInstanceClass.SCALE_INVALIDATION_THRESHOLD
                    || scaleFactorRatio <= 1 / MapCachedCanvasLayerCanvasInstanceClass.SCALE_INVALIDATION_THRESHOLD
                    || this._transform.marginRemaining < 0;
            }
            if (this.isDisplayed && !this._isInvalid) {
                this.transformCanvasElement();
            }
        }
        /**
         * Transforms this instance's canvas element.
         */
        transformCanvasElement() {
            const transform = this.transform;
            const offsetX = transform.translation[0] / transform.scale;
            const offsetY = transform.translation[1] / transform.scale;
            this.canvasTransform.transform.getChild(0).set(transform.scale, transform.scale, 0.001);
            this.canvasTransform.transform.getChild(1).set(offsetX, offsetY, 0.1);
            this.canvasTransform.transform.getChild(2).set(transform.rotation, 1e-4);
            this.canvasTransform.resolve();
        }
        /** @inheritdoc */
        invalidate() {
            this._isInvalid = true;
            this.clear();
        }
    }
    MapCachedCanvasLayerCanvasInstanceClass.SCALE_INVALIDATION_THRESHOLD = 1.2;
    MapCachedCanvasLayerCanvasInstanceClass.tempVec2_1 = new Float64Array(2);
    /**
     * A canvas map layer whose image can be cached and transformed as the map projection changes.
     */
    class MapCachedCanvasLayer extends MapCanvasLayer {
        /** @inheritdoc */
        constructor(props) {
            super(props);
            this.size = 0;
            this.referenceMargin = 0;
            this.needUpdateTransforms = false;
            this.props.overdrawFactor = Math.max(1, this.props.overdrawFactor);
        }
        /**
         * Gets the size, in pixels, of this layer's canvas.
         * @returns the size of this layer's canvas.
         */
        getSize() {
            return this.size;
        }
        /**
         * Gets the reference translation margin, in pixels, of this layer's display canvas. This value is the maximum amount
         * the display canvas can be translated in the x or y direction at a scale factor of 1 without invalidation.
         * @returns the reference translation margin of this layer's display canvas.
         */
        getReferenceMargin() {
            return this.referenceMargin;
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.updateFromProjectedSize(this.props.mapProjection.getProjectedSize());
            this.needUpdateTransforms = true;
        }
        /** @inheritdoc */
        createCanvasInstance(canvas, context, isDisplayed) {
            return new MapCachedCanvasLayerCanvasInstanceClass(canvas, context, isDisplayed, this.getReferenceMargin.bind(this));
        }
        /**
         * Updates this layer according to the current size of the projected map window.
         * @param projectedSize The size of the projected map window.
         */
        updateFromProjectedSize(projectedSize) {
            const projectedWidth = projectedSize[0];
            const projectedHeight = projectedSize[1];
            const diag = Math.hypot(projectedWidth, projectedHeight);
            this.size = diag * this.props.overdrawFactor;
            this.referenceMargin = (this.size - diag) / 2;
            this.setWidth(this.size);
            this.setHeight(this.size);
            const posX = (projectedWidth - this.size) / 2;
            const posY = (projectedHeight - this.size) / 2;
            const displayCanvas = this.display.canvas;
            displayCanvas.style.left = `${posX}px`;
            displayCanvas.style.top = `${posY}px`;
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            var _a;
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.updateFromProjectedSize(mapProjection.getProjectedSize());
                this.display.invalidate();
                (_a = this.tryGetBuffer()) === null || _a === void 0 ? void 0 : _a.invalidate();
            }
            this.needUpdateTransforms = true;
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            super.onUpdated(time, elapsed);
            if (!this.needUpdateTransforms) {
                return;
            }
            this.updateTransforms();
        }
        /**
         * Updates this layer's canvas instances' transforms.
         */
        updateTransforms() {
            var _a;
            const mapProjection = this.props.mapProjection;
            this.display.updateTransform(mapProjection);
            (_a = this.tryGetBuffer()) === null || _a === void 0 ? void 0 : _a.updateTransform(mapProjection);
            this.needUpdateTransforms = false;
        }
    }

    /**
     * A layer which draws airspaces.
     */
    class MapAirspaceLayer extends MapLayer {
        constructor() {
            var _a, _b;
            super(...arguments);
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.clipBoundsSub = VecNSubject.createFromVector(new Float64Array(4));
            this.facLoader = new FacilityLoader(FacilityRepository.getRepository(this.props.bus), async () => {
                this.searchSession = new NearestLodBoundarySearchSession(this.props.lodBoundaryCache, await this.facLoader.startNearestSearchSession(exports.FacilitySearchType.Boundary), 0.5);
                this.isAttached && this.scheduleSearch(0, true);
            });
            this.searchedAirspaces = new Map();
            this.searchDebounceDelay = (_a = this.props.searchDebounceDelay) !== null && _a !== void 0 ? _a : MapAirspaceLayer.DEFAULT_SEARCH_DEBOUNCE_DELAY;
            this.renderTimeBudget = (_b = this.props.renderTimeBudget) !== null && _b !== void 0 ? _b : MapAirspaceLayer.DEFAULT_RENDER_TIME_BUDGET;
            this.activeRenderProcess = null;
            this.renderTaskQueueHandler = {
                renderTimeBudget: this.renderTimeBudget,
                // eslint-disable-next-line jsdoc/require-jsdoc
                onStarted() {
                    // noop
                },
                // eslint-disable-next-line jsdoc/require-jsdoc
                canContinue(elapsedFrameCount, dispatchedTaskCount, timeElapsed) {
                    return timeElapsed < this.renderTimeBudget;
                },
                // eslint-disable-next-line jsdoc/require-jsdoc
                onPaused: this.onRenderPaused.bind(this),
                // eslint-disable-next-line jsdoc/require-jsdoc
                onFinished: this.onRenderFinished.bind(this),
                // eslint-disable-next-line jsdoc/require-jsdoc
                onAborted: this.onRenderAborted.bind(this)
            };
            this.searchDebounceTimer = 0;
            this.isSearchScheduled = false;
            this.needRefilter = false;
            this.isSearchBusy = false;
            this.lastDesiredSearchRadius = 0; // meters
            this.lastSearchRadius = 0; // meters
            this.isRenderScheduled = false;
            this.isBackgroundRenderScheduled = false;
            this.isDisplayInvalidated = true;
            this.isAttached = false;
        }
        /** @inheritdoc */
        onAttached() {
            this.canvasLayerRef.instance.onAttached();
            this.updateClipBounds();
            this.clippedPathStream = new ClippedPathStream(this.canvasLayerRef.instance.buffer.context, this.clipBoundsSub);
            this.props.maxSearchRadius.sub(radius => {
                const radiusMeters = radius.asUnit(UnitType.METER);
                if (radiusMeters < this.lastSearchRadius || radiusMeters > this.lastDesiredSearchRadius) {
                    this.scheduleSearch(0, false);
                }
            });
            this.props.maxSearchItemCount.sub(() => { this.scheduleSearch(0, false); });
            this.initModuleListeners();
            this.isAttached = true;
            this.searchSession && this.scheduleSearch(0, true);
        }
        /**
         * Initializes this layer's airspace module property listeners.
         */
        initModuleListeners() {
            const airspaceModule = this.props.model.getModule('airspace');
            for (const type of Object.values(airspaceModule.show)) {
                type.sub(this.onAirspaceTypeShowChanged.bind(this));
            }
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.updateClipBounds();
            }
        }
        /**
         * Updates this layer's canvas clipping bounds.
         */
        updateClipBounds() {
            const size = this.canvasLayerRef.instance.getSize();
            this.clipBoundsSub.set(-MapAirspaceLayer.CLIP_BOUNDS_BUFFER, -MapAirspaceLayer.CLIP_BOUNDS_BUFFER, size + MapAirspaceLayer.CLIP_BOUNDS_BUFFER, size + MapAirspaceLayer.CLIP_BOUNDS_BUFFER);
        }
        /**
         * Schedules a search. If a search was previously scheduled but not yet executed, this new scheduled search will
         * replace the old one.
         * @param delay The delay, in milliseconds, before the search is executed.
         * @param refilter Whether to update the search's boundary class filter.
         */
        scheduleSearch(delay, refilter) {
            if (!this.searchSession) {
                return;
            }
            this.searchDebounceTimer = delay;
            this.isSearchScheduled = true;
            this.needRefilter || (this.needRefilter = refilter);
        }
        /**
         * Schedules a render to be executed during the next update cycle.
         */
        scheduleRender() {
            this.isRenderScheduled = true;
        }
        /**
         * Searches for airspaces around the map center. After the search is complete, the list of search results is filtered
         * and, if necessary, rendered.
         * @param refilter Whether to update the search's boundary class filter.
         */
        async searchAirspaces(refilter) {
            this.isSearchBusy = true;
            const center = this.props.mapProjection.getCenter();
            const drawableDiag = this.canvasLayerRef.instance.display.canvas.width * Math.SQRT2;
            this.lastDesiredSearchRadius = UnitType.GA_RADIAN.convertTo(this.props.mapProjection.getProjectedResolution() * drawableDiag / 2, UnitType.METER);
            this.lastSearchRadius = Math.min(this.props.maxSearchRadius.get().asUnit(UnitType.METER), this.lastDesiredSearchRadius);
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const session = this.searchSession;
            refilter && session.setFilter(this.getBoundaryFilter());
            const results = await session.searchNearest(center.lat, center.lon, this.lastSearchRadius, this.props.maxSearchItemCount.get());
            for (let i = 0; i < results.added.length; i++) {
                const airspace = results.added[i];
                this.searchedAirspaces.set(airspace.facility.id, airspace);
            }
            for (let i = 0; i < results.removed.length; i++) {
                this.searchedAirspaces.delete(results.removed[i]);
            }
            this.isSearchBusy = false;
            this.scheduleRender();
        }
        /**
         * Gets the boundary class filter based on the current airspace type visibility settings.
         * @returns The boundary class filter based on the current airspace type visibility settings.
         */
        getBoundaryFilter() {
            const module = this.props.model.getModule('airspace');
            const show = module.show;
            let filter = 0;
            for (const type in show) {
                if (show[type].get()) {
                    filter |= module.showTypes[type];
                }
            }
            return filter;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        onUpdated(time, elapsed) {
            this.canvasLayerRef.instance.onUpdated(time, elapsed);
            this.updateFromInvalidation();
            this.updateScheduledRender();
            this.updateScheduledSearch(elapsed);
        }
        /**
         * Checks if the display and buffer canvases have been invalidated, and if so, clears them and schedules a render.
         */
        updateFromInvalidation() {
            const canvasLayer = this.canvasLayerRef.instance;
            const display = canvasLayer.display;
            const buffer = canvasLayer.buffer;
            const needBackgroundRender = !this.isBackgroundRenderScheduled
                && !this.activeRenderProcess
                && (display.transform.marginRemaining / display.transform.margin <= MapAirspaceLayer.BACKGROUND_RENDER_MARGIN_THRESHOLD);
            const shouldScheduleSearch = needBackgroundRender
                || display.isInvalid
                || (buffer.isInvalid && this.activeRenderProcess);
            this.isBackgroundRenderScheduled || (this.isBackgroundRenderScheduled = needBackgroundRender);
            if (display.isInvalid) {
                this.isDisplayInvalidated = true;
                this.isBackgroundRenderScheduled = false;
                display.clear();
                display.syncWithMapProjection(this.props.mapProjection);
            }
            if (buffer.isInvalid) {
                if (this.activeRenderProcess) {
                    this.activeRenderProcess.abort();
                    this.cleanUpRender();
                }
                buffer.clear();
                buffer.syncWithMapProjection(this.props.mapProjection);
            }
            if (shouldScheduleSearch) {
                this.scheduleSearch(this.searchDebounceDelay, false);
            }
        }
        /**
         * If a search is scheduled, decrements the delay timer and if necessary, executes the search.
         * @param elapsed The time elapsed, in milliseconds, since the last update.
         */
        updateScheduledSearch(elapsed) {
            if (!this.isSearchScheduled) {
                return;
            }
            this.searchDebounceTimer = Math.max(0, this.searchDebounceTimer - elapsed);
            if (this.searchDebounceTimer === 0 && !this.isSearchBusy) {
                this.searchAirspaces(this.needRefilter);
                this.isSearchScheduled = false;
                this.needRefilter = false;
            }
        }
        /**
         * Executes a render if one is scheduled.
         */
        updateScheduledRender() {
            if (!this.isRenderScheduled) {
                return;
            }
            this.startRenderProcess();
            this.isRenderScheduled = false;
            this.isBackgroundRenderScheduled = false;
        }
        /**
         * Syncs this layer's display canvas instance with the current map projection and renders this layer's airspaces to
         * the display.
         */
        startRenderProcess() {
            const canvasLayer = this.canvasLayerRef.instance;
            if (this.activeRenderProcess) {
                this.activeRenderProcess.abort();
            }
            const buffer = canvasLayer.buffer;
            buffer.clear();
            buffer.syncWithMapProjection(this.props.mapProjection);
            this.props.airspaceRenderManager.clearRegisteredAirspaces();
            for (const airspace of this.searchedAirspaces.values()) {
                if (this.isAirspaceInBounds(airspace, buffer)) {
                    this.props.airspaceRenderManager.registerAirspace(airspace);
                }
            }
            const lod = this.selectLod(this.props.mapProjection.getProjectedResolution());
            this.activeRenderProcess = this.props.airspaceRenderManager.prepareRenderProcess(buffer.geoProjection, buffer.context, this.renderTaskQueueHandler, lod, this.clippedPathStream);
            this.activeRenderProcess.start();
        }
        /**
         * Checks whether an airspace is within the projected bounds of a cached canvas instance.
         * @param airspace An airspace.
         * @param canvas A cached canvas instance.
         * @returns Whether the airspace is within the projected bounds of the cached canvas instance.
         */
        isAirspaceInBounds(airspace, canvas) {
            const corner = MapAirspaceLayer.geoPointCache[0];
            const cornerProjected = MapAirspaceLayer.vec2Cache[0];
            let minX, maxX, minY, maxY;
            canvas.geoProjection.project(corner.set(airspace.facility.topLeft.lat, airspace.facility.topLeft.long), cornerProjected);
            minX = maxX = cornerProjected[0];
            minY = maxY = cornerProjected[1];
            canvas.geoProjection.project(corner.set(airspace.facility.topLeft.lat, airspace.facility.bottomRight.long), cornerProjected);
            minX = Math.min(minX, cornerProjected[0]);
            maxX = Math.max(maxX, cornerProjected[0]);
            minY = Math.min(minY, cornerProjected[1]);
            maxY = Math.max(maxY, cornerProjected[1]);
            canvas.geoProjection.project(corner.set(airspace.facility.bottomRight.lat, airspace.facility.bottomRight.long), cornerProjected);
            minX = Math.min(minX, cornerProjected[0]);
            maxX = Math.max(maxX, cornerProjected[0]);
            minY = Math.min(minY, cornerProjected[1]);
            maxY = Math.max(maxY, cornerProjected[1]);
            canvas.geoProjection.project(corner.set(airspace.facility.bottomRight.lat, airspace.facility.topLeft.long), cornerProjected);
            minX = Math.min(minX, cornerProjected[0]);
            maxX = Math.max(maxX, cornerProjected[0]);
            minY = Math.min(minY, cornerProjected[1]);
            maxY = Math.max(maxY, cornerProjected[1]);
            const width = canvas.canvas.width;
            const height = canvas.canvas.height;
            return minX < width
                && maxX > 0
                && minY < height
                && maxY > 0;
        }
        /**
         * Selects an LOD level based on projected map resolution.
         * @param resolution A projected map resolution, in great-arc radians per pixel.
         * @returns An LOD level based on the projected map resolution.
         */
        selectLod(resolution) {
            const thresholds = this.props.lodBoundaryCache.lodDistanceThresholds;
            let i = thresholds.length - 1;
            while (i >= 0) {
                if (resolution * 2 >= thresholds[i]) {
                    break;
                }
                i--;
            }
            return i;
        }
        /**
         * Cleans up the active render process.
         */
        cleanUpRender() {
            this.canvasLayerRef.instance.buffer.reset();
            this.activeRenderProcess = null;
        }
        /**
         * Renders airspaces from the buffer to the display.
         */
        renderAirspacesToDisplay() {
            const display = this.canvasLayerRef.instance.display;
            const buffer = this.canvasLayerRef.instance.buffer;
            display.clear();
            display.syncWithCanvasInstance(buffer);
            this.canvasLayerRef.instance.copyBufferToDisplay();
        }
        /**
         * This method is called when the airspace render process pauses.
         */
        onRenderPaused() {
            if (this.isDisplayInvalidated) {
                this.renderAirspacesToDisplay();
            }
        }
        /**
         * This method is called when the airspace render process finishes.
         */
        onRenderFinished() {
            this.renderAirspacesToDisplay();
            this.cleanUpRender();
            this.isDisplayInvalidated = false;
        }
        /**
         * This method is called when the airspace render process is aborted.
         */
        onRenderAborted() {
            this.cleanUpRender();
        }
        /**
         * This method is called when an airspace show property changes.
         */
        onAirspaceTypeShowChanged() {
            this.scheduleSearch(0, true);
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent(MapCachedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, useBuffer: true, overdrawFactor: Math.SQRT2 }));
        }
    }
    MapAirspaceLayer.DEFAULT_SEARCH_DEBOUNCE_DELAY = 500; // milliseconds
    MapAirspaceLayer.DEFAULT_RENDER_TIME_BUDGET = 0.2; // milliseconds per frame
    MapAirspaceLayer.BACKGROUND_RENDER_MARGIN_THRESHOLD = 0.1; // relative to total margin
    MapAirspaceLayer.CLIP_BOUNDS_BUFFER = 10; // number of pixels from edge of canvas to extend the clipping bounds, in pixels
    MapAirspaceLayer.geoPointCache = [new GeoPoint(0, 0)];
    MapAirspaceLayer.vec2Cache = [new Float64Array(2)];

    /**
     * A utility class for creating number formatters.
     *
     * Each number formatter is a function which generates output strings from input numeric values. The formatting
     * behavior of a formatter is defined by its options. Please refer to the {@link NumberFormatterOptions} type
     * documentation for more information on each individual option.
     */
    class NumberFormatter {
        /**
         * Formats a number to a string.
         * @param number The number to format.
         * @param opts Options describing how to format the number.
         * @returns The formatted string representation of the specified number.
         */
        static formatNumber(number, opts) {
            if (isNaN(number)) {
                opts.cachedNumber = undefined;
                opts.cachedString = undefined;
                return opts.nanString;
            }
            else if (!isFinite(number)) {
                opts.cachedNumber = undefined;
                opts.cachedString = undefined;
                return number > 0 ? opts.posInfinityString : opts.negInfinityString;
            }
            const { precision, roundFunc, hysteresisOffsetLower, hysteresisOffsetUpper, maxDigits, forceDecimalZeroes, pad, showCommas, useMinusSign, forceSign, hideSign, cache } = opts;
            if (opts.cachedNumber !== undefined
                && opts.cachedString !== undefined
                && (hysteresisOffsetLower !== 0 || hysteresisOffsetUpper !== 0)) {
                if (opts.round > 0) {
                    if (number > opts.cachedNumber + hysteresisOffsetLower && number <= opts.cachedNumber + hysteresisOffsetUpper) {
                        return opts.cachedString;
                    }
                }
                else {
                    if (number >= opts.cachedNumber + hysteresisOffsetLower && number < opts.cachedNumber + hysteresisOffsetUpper) {
                        return opts.cachedString;
                    }
                }
            }
            let rounded = number;
            if (precision !== 0) {
                rounded = roundFunc(number / precision) * precision;
            }
            const absRounded = Math.abs(rounded);
            if (cache) {
                if (opts.cachedString !== undefined && opts.cachedNumber === rounded) {
                    return opts.cachedString;
                }
                opts.cachedNumber = rounded;
            }
            let formatted;
            if (precision !== 0) {
                const precisionString = `${precision}`;
                const decimalIndex = precisionString.indexOf('.');
                if (decimalIndex >= 0) {
                    formatted = absRounded.toFixed(precisionString.length - decimalIndex - 1);
                }
                else {
                    formatted = `${absRounded}`;
                }
            }
            else {
                formatted = `${absRounded}`;
            }
            let decimalIndex = formatted.indexOf('.');
            if (!forceDecimalZeroes && decimalIndex >= 0) {
                formatted = formatted.replace(NumberFormatter.TRAILING_ZERO_REGEX, '');
                if (formatted.indexOf('.') == formatted.length - 1) {
                    formatted = formatted.substring(0, formatted.length - 1);
                }
            }
            let needRecalcPadAndMaxDigits = false;
            let calcPadIterCount = 0;
            do {
                decimalIndex = formatted.indexOf('.');
                // Add (or subtract) leading zeroes as necessary.
                if (pad === 0) {
                    if (decimalIndex > 0) {
                        formatted = formatted.replace(NumberFormatter.LEADING_ZERO_REGEX, '.');
                    }
                }
                else if (pad > 1) {
                    if (decimalIndex < 0) {
                        decimalIndex = formatted.length;
                    }
                    if (decimalIndex < pad) {
                        formatted = formatted.padStart(pad + formatted.length - decimalIndex, '0');
                    }
                }
                // Check if the formatted number exceeds the maximum digit count. If so, reduce the number of digits to the
                // right of the decimal place as necessary.
                decimalIndex = formatted.indexOf('.');
                if (decimalIndex >= 0 && formatted.length - 1 > maxDigits) {
                    const desiredRightDigits = Math.max(maxDigits - decimalIndex, 0);
                    const shiftPrecision = Math.pow(0.1, desiredRightDigits);
                    rounded = roundFunc(rounded / shiftPrecision) * shiftPrecision;
                    formatted = Math.abs(rounded).toFixed(desiredRightDigits);
                    // Reformatting may change the number of digits to the left of the decimal point required to represent the
                    // number. Additionally, reformatting will mess up any left zero-padding we set up earlier. For both reasons,
                    // we need to recalculate the padding and number of decimal places again.
                    needRecalcPadAndMaxDigits = pad !== 0 || desiredRightDigits > 0;
                }
                else {
                    needRecalcPadAndMaxDigits = false;
                }
                calcPadIterCount++;
            } while (needRecalcPadAndMaxDigits && calcPadIterCount < 2);
            if (showCommas) {
                const parts = formatted.split('.');
                parts[0] = parts[0].replace(NumberFormatter.COMMAS_REGEX, ',');
                formatted = parts.join('.');
            }
            const sign = rounded < 0 ? -1 : 1;
            const signText = sign === -1
                ? useMinusSign ? '−' : '-'
                : '+';
            if (!hideSign && (forceSign || signText !== '+')) {
                formatted = signText + formatted;
            }
            if (cache) {
                opts.cachedString = formatted;
            }
            return formatted;
        }
        /**
         * Resolves an internal options object from a partial options object. Any option not explicitly defined by the
         * partial options object will revert to its default value.
         * @param options A partial options object.
         * @returns A new internal options object containing the full set of options resolved from the specified partial
         * options object.
         */
        static resolveOptions(options) {
            var _a;
            const resolved = {};
            for (const key in NumberFormatter.DEFAULT_OPTIONS) {
                resolved[key] = (_a = options === null || options === void 0 ? void 0 : options[key]) !== null && _a !== void 0 ? _a : NumberFormatter.DEFAULT_OPTIONS[key];
            }
            let hysteresisLower;
            let hysteresisUpper;
            if (typeof resolved.hysteresis === 'number') {
                hysteresisLower = hysteresisUpper = Math.max(0, resolved.hysteresis);
            }
            else {
                hysteresisLower = Math.max(0, resolved.hysteresis[0]);
                hysteresisUpper = Math.max(0, resolved.hysteresis[1]);
            }
            delete resolved.hysteresis;
            const resolvedInt = resolved;
            if (resolvedInt.round > 0) {
                resolvedInt.roundFunc = Math.ceil;
                resolvedInt.hysteresisOffsetLower = -(resolvedInt.precision + hysteresisLower);
                resolvedInt.hysteresisOffsetUpper = hysteresisUpper;
            }
            else if (resolvedInt.round < 0) {
                resolvedInt.roundFunc = Math.floor;
                resolvedInt.hysteresisOffsetLower = -hysteresisLower;
                resolvedInt.hysteresisOffsetUpper = resolvedInt.precision + hysteresisUpper;
            }
            else {
                resolvedInt.roundFunc = Math.round;
                resolvedInt.hysteresisOffsetLower = -(resolvedInt.precision * 0.5 + hysteresisLower);
                resolvedInt.hysteresisOffsetUpper = resolvedInt.precision * 0.5 + hysteresisUpper;
            }
            resolvedInt.cache || (resolvedInt.cache = resolvedInt.hysteresisOffsetLower !== 0 || resolvedInt.hysteresisOffsetUpper !== 0);
            return resolvedInt;
        }
        /**
         * Creates a function which formats numeric values to strings. The formatting behavior of the function can be
         * customized using a number of options. Please refer to the {@link NumberFormatterOptions} type documentation for
         * more information on each individual option.
         * @param options Options with which to customize the formatter.
         * @returns A function which formats numeric values to strings.
         */
        static create(options) {
            const optsToUse = NumberFormatter.resolveOptions(options);
            return (number) => {
                return NumberFormatter.formatNumber(number, optsToUse);
            };
        }
    }
    NumberFormatter.DEFAULT_OPTIONS = {
        precision: 0,
        round: exports.Rounding.Nearest,
        hysteresis: 0,
        maxDigits: Infinity,
        forceDecimalZeroes: true,
        pad: 1,
        showCommas: false,
        useMinusSign: false,
        forceSign: false,
        hideSign: false,
        nanString: 'NaN',
        posInfinityString: 'Infinity',
        negInfinityString: '-Infinity',
        cache: false
    };
    NumberFormatter.TRAILING_ZERO_REGEX = /0+$/;
    NumberFormatter.LEADING_ZERO_REGEX = /^0\./;
    NumberFormatter.COMMAS_REGEX = /\B(?=(\d{3})+(?!\d))/g;

    /**
     * A path stream which builds SVG path strings from its input path commands.
     */
    class SvgPathStream {
        /**
         * Constructor.
         * @param precision The precision of this stream. All coordinates will be rounded to this stream's precision when
         * building the SVG path string. A value of `0` indicates infinite precision. Defaults to `0`.
         */
        constructor(precision = 0) {
            this.svgPath = '';
            this.firstPoint = new Float64Array([NaN, NaN]);
            this.prevPoint = new Float64Array([NaN, NaN]);
            this.precision = precision;
            this.formatter = NumberFormatter.create({ precision, forceDecimalZeroes: false });
        }
        /**
         * Gets the SVG path string describing all path commands consumed by this stream since the last call to
         * `beginPath()`.
         * @returns The SVG path string describing all path commands consumed by this stream since the last call to
         * `beginPath()`.
         */
        getSvgPath() {
            return this.svgPath.trim();
        }
        /**
         * Gets the precision of this stream. All coordinates will be rounded to this stream's precision when building the
         * SVG path string. A value of `0` indicates infinite precision.
         * @returns The precision of this stream.
         */
        getPrecision() {
            return this.precision;
        }
        /**
         * Sets the precision of this stream. All coordinates will be rounded to this stream's precision when building the
         * SVG path string. A value of `0` indicates infinite precision.
         * @param precision The precision of this stream. Negative numbers will be converted to their absolute values.
         */
        setPrecision(precision) {
            this.precision = Math.abs(precision);
            this.formatter = NumberFormatter.create({ precision: this.precision, forceDecimalZeroes: false });
        }
        /** @inheritdoc */
        beginPath() {
            this.reset();
        }
        /** @inheritdoc */
        moveTo(x, y) {
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (isNaN(this.firstPoint[0])) {
                Vec2Math.set(x, y, this.firstPoint);
            }
            this.svgPath += `M ${this.formatter(x)} ${this.formatter(y)} `;
            Vec2Math.set(x, y, this.prevPoint);
        }
        /** @inheritdoc */
        lineTo(x, y) {
            if (!(isFinite(x) && isFinite(y))) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            this.svgPath += `L ${this.formatter(x)} ${this.formatter(y)} `;
            Vec2Math.set(x, y, this.prevPoint);
        }
        /** @inheritdoc */
        bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
            if (!(isFinite(x) && isFinite(y) && isFinite(cp1x) && isFinite(cp1y) && isFinite(cp2x) && isFinite(cp2y))) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            this.svgPath += `C ${this.formatter(cp1x)} ${this.formatter(cp1y)} ${this.formatter(cp2x)} ${this.formatter(cp2y)} ${this.formatter(x)} ${this.formatter(y)} `;
            Vec2Math.set(x, y, this.prevPoint);
        }
        /** @inheritdoc */
        quadraticCurveTo(cpx, cpy, x, y) {
            if (!(isFinite(x) && isFinite(y) && isFinite(cpx) && isFinite(cpy))) {
                return;
            }
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(x, y);
                return;
            }
            this.svgPath += `Q ${this.formatter(cpx)} ${this.formatter(cpy)} ${this.formatter(x)} ${this.formatter(y)} `;
            Vec2Math.set(x, y, this.prevPoint);
        }
        /** @inheritdoc */
        arc(x, y, radius, startAngle, endAngle, counterClockwise) {
            if (!(isFinite(x) && isFinite(y) && isFinite(radius) && isFinite(startAngle) && isFinite(endAngle))) {
                return;
            }
            const directionSign = counterClockwise ? -1 : 1;
            if (Math.sign(endAngle - startAngle) !== directionSign) {
                // Replicate behavior of canvas context arc() when the sign of the difference between start and end angles
                // doesn't match the counterClockwise flag.
                const angleDiff = counterClockwise ? MathUtils.diffAngle(endAngle, startAngle) : MathUtils.diffAngle(startAngle, endAngle);
                endAngle = startAngle + angleDiff * directionSign;
            }
            // Clamp to 2pi because we don't need to draw anything past a full circle.
            const angularWidth = Math.min(MathUtils.TWO_PI, (endAngle - startAngle) * directionSign);
            if (angularWidth === MathUtils.TWO_PI) {
                // SVG arc commands cannot draw a full circle, so we need to split the circle into two half circles
                const midAngle = startAngle + Math.PI * directionSign;
                this.arc(x, y, radius, startAngle, midAngle, counterClockwise);
                this.arc(x, y, radius, midAngle, startAngle, counterClockwise);
                return;
            }
            const startPoint = Vec2Math.add(Vec2Math.set(x, y, SvgPathStream.vec2Cache[0]), Vec2Math.setFromPolar(radius, startAngle, SvgPathStream.vec2Cache[2]), SvgPathStream.vec2Cache[0]);
            if (isNaN(this.prevPoint[0])) {
                this.moveTo(startPoint[0], startPoint[1]);
            }
            else if (!Vec2Math.equals(this.prevPoint, startPoint)) {
                this.lineTo(startPoint[0], startPoint[1]);
            }
            const endPoint = Vec2Math.add(Vec2Math.set(x, y, SvgPathStream.vec2Cache[1]), Vec2Math.setFromPolar(radius, endAngle, SvgPathStream.vec2Cache[2]), SvgPathStream.vec2Cache[1]);
            const radiusString = this.formatter(radius);
            this.svgPath += `A ${radiusString} ${radiusString} 0 ${angularWidth > Math.PI ? 1 : 0} ${counterClockwise ? 0 : 1} ${this.formatter(endPoint[0])} ${this.formatter(endPoint[1])} `;
            Vec2Math.copy(endPoint, this.prevPoint);
        }
        /** @inheritdoc */
        closePath() {
            if (!isNaN(this.firstPoint[0])) {
                this.lineTo(this.firstPoint[0], this.firstPoint[1]);
            }
        }
        /**
         * Resets the state of this stream.
         */
        reset() {
            Vec2Math.set(NaN, NaN, this.firstPoint);
            Vec2Math.set(NaN, NaN, this.prevPoint);
            this.svgPath = '';
        }
    }
    SvgPathStream.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2), new Float64Array(2)];

    /**
     * A collection of common keys used by the MapSystem API.
     */
    class MapSystemKeys {
    }
    MapSystemKeys.TargetControl = 'targetControlModerator';
    MapSystemKeys.RotationControl = 'rotationControlModerator';
    MapSystemKeys.RangeControl = 'rangeControlModerator';
    MapSystemKeys.ClockUpdate = 'clockUpdate';
    MapSystemKeys.OwnAirplaneProps = 'ownAirplaneProps';
    MapSystemKeys.AutopilotProps = 'autopilotProps';
    MapSystemKeys.AltitudeArc = 'altitudeArc';
    MapSystemKeys.TerrainColors = 'terrainColors';
    MapSystemKeys.Weather = 'weather';
    MapSystemKeys.FollowAirplane = 'followAirplane';
    MapSystemKeys.Rotation = 'rotation';
    MapSystemKeys.OwnAirplaneIcon = 'ownAirplaneIcon';
    MapSystemKeys.OwnAirplaneIconOrientation = 'ownAirplaneIconOrientation';
    MapSystemKeys.TextLayer = 'text';
    MapSystemKeys.TextManager = 'textManager';
    MapSystemKeys.Bing = 'bing';
    MapSystemKeys.WaypointRenderer = 'waypointRenderer';
    MapSystemKeys.IconFactory = 'iconFactory';
    MapSystemKeys.LabelFactory = 'labelFactory';
    MapSystemKeys.NearestWaypoints = 'nearestWaypoints';
    MapSystemKeys.FlightPlan = 'flightPlan';
    MapSystemKeys.FlightPlanner = 'flightPlanner';
    MapSystemKeys.FlightPathRenderer = 'flightPathRenderer';
    MapSystemKeys.Airspace = 'airspace';
    MapSystemKeys.AirspaceManager = 'airspaceRenderManager';
    MapSystemKeys.Traffic = 'traffic';
    MapSystemKeys.DataIntegrity = 'dataIntegrity';
    MapSystemKeys.PlanAirportsLayer = 'plan-airports';
    MapSystemKeys.WaypointDisplayController = 'WaypointDisplayController';

    /**
     * A map layer which displays an altitude intercept arc.
     */
    class MapAltitudeArcLayer extends MapLayer {
        constructor() {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            super(...arguments);
            this.layerRef = exports.FSComponent.createRef();
            this.arcAngularWidth = ((_a = this.props.arcAngularWidth) !== null && _a !== void 0 ? _a : MapAltitudeArcLayer.DEFAULT_ARC_ANGULAR_WIDTH) * Avionics.Utils.DEG2RAD;
            this.arcRadius = (_b = this.props.arcRadius) !== null && _b !== void 0 ? _b : MapAltitudeArcLayer.DEFAULT_ARC_RADIUS;
            this.strokeWidth = (_c = this.props.strokeWidth) !== null && _c !== void 0 ? _c : MapAltitudeArcLayer.DEFAULT_STROKE_WIDTH;
            this.strokeStyle = (_d = this.props.strokeStyle) !== null && _d !== void 0 ? _d : MapAltitudeArcLayer.DEFAULT_STROKE_STYLE;
            this.strokeLineCap = (_e = this.props.strokeLineCap) !== null && _e !== void 0 ? _e : MapAltitudeArcLayer.DEFAULT_STROKE_LINECAP;
            this.outlineWidth = (_f = this.props.outlineWidth) !== null && _f !== void 0 ? _f : MapAltitudeArcLayer.DEFAULT_OUTLINE_WIDTH;
            this.outlineStyle = (_g = this.props.outlineStyle) !== null && _g !== void 0 ? _g : MapAltitudeArcLayer.DEFAULT_OUTLINE_STYLE;
            this.outlineLineCap = (_h = this.props.outlineLineCap) !== null && _h !== void 0 ? _h : MapAltitudeArcLayer.DEFAULT_OUTLINE_LINECAP;
            this.ownAirplanePropsModule = this.props.model.getModule(MapSystemKeys.OwnAirplaneProps);
            this.autopilotModule = this.props.model.getModule(MapSystemKeys.AutopilotProps);
            this.vsPrecisionFpm = ('isSubscribable' in this.props.verticalSpeedPrecision)
                ? this.vsPrecisionMap = this.props.verticalSpeedPrecision.map(v => v.asUnit(UnitType.FPM))
                : Subject.create(this.props.verticalSpeedPrecision.asUnit(UnitType.FPM));
            this.vsThresholdFpm = ('isSubscribable' in this.props.verticalSpeedThreshold)
                ? this.vsThresholdMap = this.props.verticalSpeedThreshold.map(v => v.asUnit(UnitType.FPM))
                : Subject.create(this.props.verticalSpeedThreshold.asUnit(UnitType.FPM));
            this.altDevThresholdFeet = ('isSubscribable' in this.props.altitudeDeviationThreshold)
                ? this.altDevThresholdMap = this.props.altitudeDeviationThreshold.map(v => v.asUnit(UnitType.FOOT))
                : Subject.create(this.props.altitudeDeviationThreshold.asUnit(UnitType.FOOT));
            this.vsFpm = this.ownAirplanePropsModule.verticalSpeed.map(vs => vs.asUnit(UnitType.FPM));
            this.vsFpmQuantized = MappedSubject.create(([vsFpm, precision]) => {
                return Math.round(vsFpm / precision) * precision;
            }, this.vsFpm, this.vsPrecisionFpm);
            this.projectedPlanePosition = Vec2Subject.create(Vec2Math.create());
            this.projectPlanePositionHandler = () => {
                const projected = this.props.mapProjection.project(this.ownAirplanePropsModule.position.get(), MapAltitudeArcLayer.vec2Cache[0]);
                this.projectedPlanePosition.set(projected);
            };
            this.isArcVisibleDynamic = MappedSubject.create(([vsFpm, alt, selectedAlt, vsThreshold, altDevThresholdFeet]) => {
                if (Math.abs(vsFpm) < vsThreshold) {
                    return false;
                }
                const altDevFeet = selectedAlt.asUnit(UnitType.FOOT) - alt.asUnit(UnitType.FOOT);
                return Math.abs(altDevFeet) >= altDevThresholdFeet && altDevFeet * vsFpm > 0;
            }, this.vsFpmQuantized, this.ownAirplanePropsModule.altitude, this.autopilotModule.selectedAltitude, this.vsThresholdFpm, this.altDevThresholdFeet).pause();
            this.projectedArcPosition = Vec2Subject.create(Vec2Math.create());
            this.projectedArcAngle = Subject.create(0);
            this.needUpdate = false;
            this.subscriptions = [];
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            var _a;
            (_a = this.layerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.setVisible(isVisible);
            if (isVisible) {
                this.needUpdate = true;
            }
        }
        /** @inheritdoc */
        onAttached() {
            var _a, _b;
            this.layerRef.instance.onAttached();
            this.subscriptions.push(this.ownAirplanePropsModule.position.sub(this.projectPlanePositionHandler));
            const scheduleUpdate = () => { this.needUpdate = true; };
            const altitudeArcModule = this.props.model.getModule(MapSystemKeys.AltitudeArc);
            const dataIntegrityModule = this.props.model.getModule(MapSystemKeys.DataIntegrity);
            this.isArcVisibleStatic = MappedSubject.create(([show, isGpsValid, isAdcValid]) => {
                return show && isGpsValid && isAdcValid;
            }, altitudeArcModule.show, (_a = dataIntegrityModule === null || dataIntegrityModule === void 0 ? void 0 : dataIntegrityModule.gpsSignalValid) !== null && _a !== void 0 ? _a : Subject.create(true), (_b = dataIntegrityModule === null || dataIntegrityModule === void 0 ? void 0 : dataIntegrityModule.adcSignalValid) !== null && _b !== void 0 ? _b : Subject.create(true));
            const isArcVisibleDynamicSub = this.isArcVisibleDynamic.sub(isVisible => { this.setVisible(isVisible); }, false, true);
            this.isArcVisibleStatic.sub(isVisible => {
                if (isVisible) {
                    this.isArcVisibleDynamic.resume();
                    isArcVisibleDynamicSub.resume(true);
                }
                else {
                    this.isArcVisibleDynamic.pause();
                    isArcVisibleDynamicSub.pause();
                    this.setVisible(false);
                }
            }, true);
            this.subscriptions.push(this.projectedPlanePosition.sub(scheduleUpdate), this.ownAirplanePropsModule.trackTrue.sub(scheduleUpdate), this.ownAirplanePropsModule.groundSpeed.sub(scheduleUpdate), this.ownAirplanePropsModule.altitude.sub(scheduleUpdate));
            this.vsFpmQuantized.sub(scheduleUpdate);
            this.subscriptions.push(this.autopilotModule.selectedAltitude.sub(scheduleUpdate, true));
            this.layerRef.instance.setVisible(this.isVisible());
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.layerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            this.projectPlanePositionHandler();
            this.needUpdate = true;
        }
        /** @inheritdoc */
        onUpdated() {
            if (!this.needUpdate || !this.isVisible()) {
                return;
            }
            const track = this.ownAirplanePropsModule.trackTrue.get();
            const groundSpeed = this.ownAirplanePropsModule.groundSpeed.get();
            const altitude = this.ownAirplanePropsModule.altitude.get();
            const selectedAltitude = this.autopilotModule.selectedAltitude.get();
            const vsFpm = this.vsFpmQuantized.get();
            const timeToAltitudeMinute = (selectedAltitude.asUnit(UnitType.FOOT) - altitude.asUnit(UnitType.FOOT)) / vsFpm;
            const distanceToAltitudeFeet = groundSpeed.asUnit(UnitType.FPM) * timeToAltitudeMinute;
            const distancePx = UnitType.FOOT.convertTo(distanceToAltitudeFeet, UnitType.GA_RADIAN) / this.props.mapProjection.getProjectedResolution();
            const projectedTrackAngle = track * Avionics.Utils.DEG2RAD + this.props.mapProjection.getRotation() - MathUtils.HALF_PI;
            const projectedPlanePos = this.projectedPlanePosition.get();
            const projectedArcPos = Vec2Math.add(Vec2Math.setFromPolar(distancePx, projectedTrackAngle, MapAltitudeArcLayer.vec2Cache[0]), projectedPlanePos, MapAltitudeArcLayer.vec2Cache[0]);
            this.projectedArcPosition.set(projectedArcPos);
            this.projectedArcAngle.set(projectedTrackAngle);
            this.layerRef.instance.onUpdated();
            this.needUpdate = false;
        }
        /** @inheritdoc */
        render() {
            const props = {
                ref: this.layerRef,
                model: this.props.model,
                mapProjection: this.props.mapProjection,
                arcAngularWidth: this.arcAngularWidth,
                arcRadius: this.arcRadius,
                strokeWidth: this.strokeWidth,
                strokeStyle: this.strokeStyle,
                strokeLineCap: this.strokeLineCap,
                outlineWidth: this.outlineWidth,
                outlineStyle: this.outlineStyle,
                outlineLineCap: this.outlineLineCap,
                projectedArcPosition: this.projectedArcPosition,
                projectedArcAngle: this.projectedArcAngle,
                class: this.props.class,
            };
            return this.props.renderMethod === 'canvas'
                ? (exports.FSComponent.buildComponent(MapAltitudeArcCanvasLayer, Object.assign({}, props))) : (exports.FSComponent.buildComponent(MapAltitudeArcSvgLayer, Object.assign({}, props)));
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b, _c, _d, _e;
            (_a = this.layerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.vsPrecisionMap) === null || _b === void 0 ? void 0 : _b.destroy();
            (_c = this.vsThresholdMap) === null || _c === void 0 ? void 0 : _c.destroy();
            (_d = this.altDevThresholdMap) === null || _d === void 0 ? void 0 : _d.destroy();
            this.vsFpm.destroy();
            (_e = this.isArcVisibleStatic) === null || _e === void 0 ? void 0 : _e.destroy();
            this.isArcVisibleDynamic.destroy();
            this.subscriptions.forEach(sub => sub.destroy());
            super.destroy();
        }
    }
    MapAltitudeArcLayer.DEFAULT_ARC_ANGULAR_WIDTH = 60; // degrees
    MapAltitudeArcLayer.DEFAULT_ARC_RADIUS = 64; // px
    MapAltitudeArcLayer.DEFAULT_STROKE_WIDTH = 2; // px
    MapAltitudeArcLayer.DEFAULT_STROKE_STYLE = 'cyan';
    MapAltitudeArcLayer.DEFAULT_STROKE_LINECAP = 'butt';
    MapAltitudeArcLayer.DEFAULT_OUTLINE_WIDTH = 1; // px
    MapAltitudeArcLayer.DEFAULT_OUTLINE_STYLE = '#505050';
    MapAltitudeArcLayer.DEFAULT_OUTLINE_LINECAP = 'butt';
    MapAltitudeArcLayer.vec2Cache = [new Float64Array(2), new Float64Array(2)];
    /**
     * A map layer which draws an altitude intercept arc using canvas.
     */
    class MapAltitudeArcCanvasLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.arcHalfAngularWidth = this.props.arcAngularWidth / 2;
            this.totalArcThickness = this.props.strokeWidth + this.props.outlineWidth * 2;
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.subscriptions = [];
            this.needUpdate = false;
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            var _a, _b;
            if (isVisible) {
                this.needUpdate = true;
            }
            else {
                (_b = (_a = this.canvasLayerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.tryGetDisplay()) === null || _b === void 0 ? void 0 : _b.clear();
            }
        }
        /** @inheritdoc */
        onAttached() {
            this.canvasLayerRef.instance.onAttached();
            const scheduleUpdate = () => { this.needUpdate = true; };
            this.subscriptions.push(this.props.projectedArcPosition.sub(scheduleUpdate, false), this.props.projectedArcAngle.sub(scheduleUpdate, false));
            this.needUpdate = true;
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
        }
        /** @inheritdoc */
        onUpdated() {
            if (!this.needUpdate || !this.isVisible()) {
                return;
            }
            const arcPos = this.props.projectedArcPosition.get();
            const display = this.canvasLayerRef.instance.display;
            display.clear();
            // Do not draw the arc if it is out of bounds.
            const projectedSize = this.props.mapProjection.getProjectedSize();
            const arcX = arcPos[0];
            const arcY = arcPos[1];
            const twiceRadius = this.props.arcRadius * 2;
            if (arcX <= -twiceRadius
                || arcX >= projectedSize[0] + twiceRadius
                || arcY <= -twiceRadius
                || arcY >= projectedSize[1] + twiceRadius) {
                return;
            }
            display.context.beginPath();
            const projectedArcAngle = this.props.projectedArcAngle.get();
            const center = Vec2Math.add(Vec2Math.setFromPolar(-this.props.arcRadius, projectedArcAngle, MapAltitudeArcCanvasLayer.vec2Cache[0]), arcPos, MapAltitudeArcCanvasLayer.vec2Cache[0]);
            const arcStart = Vec2Math.add(Vec2Math.setFromPolar(this.props.arcRadius, projectedArcAngle - this.arcHalfAngularWidth, MapAltitudeArcCanvasLayer.vec2Cache[1]), center, MapAltitudeArcCanvasLayer.vec2Cache[1]);
            display.context.moveTo(arcStart[0], arcStart[1]);
            display.context.arc(center[0], center[1], this.props.arcRadius, projectedArcAngle - this.arcHalfAngularWidth, projectedArcAngle + this.arcHalfAngularWidth);
            if (this.props.outlineWidth > 0) {
                display.context.lineWidth = this.totalArcThickness;
                display.context.strokeStyle = this.props.outlineStyle;
                display.context.lineCap = this.props.outlineLineCap;
                display.context.stroke();
            }
            if (this.props.strokeWidth > 0) {
                display.context.lineWidth = this.props.strokeWidth;
                display.context.strokeStyle = this.props.strokeStyle;
                display.context.lineCap = this.props.strokeLineCap;
                display.context.stroke();
            }
            this.needUpdate = false;
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, class: this.props.class }));
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.canvasLayerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            this.subscriptions.forEach(sub => sub.destroy());
            super.destroy();
        }
    }
    MapAltitudeArcCanvasLayer.vec2Cache = [new Float64Array(2), new Float64Array(2)];
    /**
     * A map layer which draws an altitude intercept arc using SVG.
     */
    class MapAltitudeArcSvgLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.arcHalfAngularWidth = this.props.arcAngularWidth / 2;
            this.totalArcThickness = this.props.strokeWidth + this.props.outlineWidth * 2;
            this.width = this.props.arcRadius * (1 - Math.cos(this.arcHalfAngularWidth)) + this.totalArcThickness + 2;
            this.height = 2 * this.props.arcRadius * Math.sin(Math.min(this.arcHalfAngularWidth, MathUtils.HALF_PI)) + this.totalArcThickness + 2;
            this.svgStyle = ObjectSubject.create({
                'display': '',
                'position': 'absolute',
                'left': `${(this.totalArcThickness / 2 + 1) - this.width}px`,
                'top': `${-this.height / 2}px`,
                'width': `${this.width}px`,
                'height': `${this.height}px`,
                'transform': 'translate3d(0px, 0px, 0px) rotate(0rad)',
                'transform-origin': `${this.width - (this.totalArcThickness / 2 + 1)}px ${this.height / 2}px`
            });
            this.svgTransform = CssTransformBuilder.concat(CssTransformBuilder.translate3d('px'), CssTransformBuilder.rotate('rad'));
            this.needUpdate = false;
            this.subscriptions = [];
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            if (isVisible) {
                this.needUpdate = true;
            }
            else {
                this.svgStyle.set('display', 'none');
            }
        }
        /** @inheritdoc */
        onAttached() {
            const scheduleUpdate = () => { this.needUpdate = true; };
            this.subscriptions.push(this.props.projectedArcPosition.sub(scheduleUpdate, false), this.props.projectedArcAngle.sub(scheduleUpdate, false));
        }
        /** @inheritdoc */
        onUpdated() {
            if (!this.needUpdate || !this.isVisible()) {
                return;
            }
            const arcPos = this.props.projectedArcPosition.get();
            // Hide the arc if it is out of bounds.
            const projectedSize = this.props.mapProjection.getProjectedSize();
            const arcX = arcPos[0];
            const arcY = arcPos[1];
            const twiceRadius = this.props.arcRadius * 2;
            if (arcX <= -twiceRadius
                || arcX >= projectedSize[0] + twiceRadius
                || arcY <= -twiceRadius
                || arcY >= projectedSize[1] + twiceRadius) {
                this.svgStyle.set('display', 'none');
            }
            else {
                this.svgStyle.set('display', '');
                this.svgTransform.getChild(0).set(arcX, arcY, 0, 0.1);
                this.svgTransform.getChild(1).set(this.props.projectedArcAngle.get(), 1e-4);
                this.svgStyle.set('transform', this.svgTransform.resolve());
            }
            this.needUpdate = false;
        }
        /** @inheritdoc */
        render() {
            const svgPathStream = new SvgPathStream(0.01);
            const transformPathStream = new AffineTransformPathStream(svgPathStream);
            // Top of the arc is at (0, 0), so the center is at (-radius, 0).
            transformPathStream.beginPath();
            transformPathStream
                .addRotation(-this.arcHalfAngularWidth)
                .addTranslation(-this.props.arcRadius, 0);
            transformPathStream.moveTo(this.props.arcRadius, 0);
            transformPathStream.arc(0, 0, this.props.arcRadius, 0, this.props.arcAngularWidth);
            const path = svgPathStream.getSvgPath();
            return (exports.FSComponent.buildComponent("svg", { viewBox: `${(this.totalArcThickness / 2 + 1) - this.width} ${-this.height / 2} ${this.width} ${this.height}`, style: this.svgStyle, class: this.props.class },
                exports.FSComponent.buildComponent("path", { d: path, fill: "none", stroke: this.props.outlineStyle, "stroke-width": this.totalArcThickness, "stroke-linecap": this.props.outlineLineCap }),
                exports.FSComponent.buildComponent("path", { d: path, fill: 'none', stroke: this.props.strokeStyle, "stroke-width": this.props.strokeWidth, "stroke-linecap": this.props.strokeLineCap })));
        }
        /** @inheritdoc */
        destroy() {
            this.subscriptions.forEach(sub => sub.destroy());
            super.destroy();
        }
    }

    /// <reference types="@microsoft/msfs-types/js/common" />
    /// <reference types="@microsoft/msfs-types/js/types" />
    /// <reference types="@microsoft/msfs-types/js/netbingmap" />
    /**
     * A FSComponent that display the MSFS Bing Map, weather radar, and 3D terrain.
     */
    class MapBingLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.bingRef = exports.FSComponent.createRef();
            this.wrapperStyle = ObjectSubject.create({
                'position': 'absolute',
                'left': '0px',
                'top': '0px',
                'width': '0px',
                'height': '0px',
                'display': '',
                'transform': '',
                'opacity': '',
            });
            this.resolution = Vec2Subject.create(Vec2Math.create(1024, 1024));
            this.rotationTransform = CssTransformBuilder.rotate('rad');
            /** The length of this layer's diagonal, in pixels. */
            this.size = 0;
            this.needUpdate = false;
            this.subscriptions = [];
        }
        /** @inheritDoc */
        onVisibilityChanged(isVisible) {
            this.wrapperStyle.set('display', isVisible ? '' : 'none');
        }
        /** @inheritDoc */
        onAttached() {
            this.updateFromProjectedSize(this.props.mapProjection.getProjectedSize());
            if (this.props.opacity) {
                if (SubscribableUtils.isSubscribable(this.props.opacity)) {
                    this.subscriptions.push(this.props.opacity.sub((v) => {
                        this.wrapperStyle.set('opacity', v.toString());
                    }, true));
                }
                else {
                    this.wrapperStyle.set('opacity', this.props.opacity.toString());
                }
            }
            if (this.props.wxrMode) {
                this.subscriptions.push(this.props.wxrMode.sub(() => {
                    this.updateFromProjectedSize(this.props.mapProjection.getProjectedSize());
                    this.needUpdate = true;
                }));
            }
        }
        /** @inheritDoc */
        onWake() {
            this.bingRef.instance.wake();
        }
        /** @inheritDoc */
        onSleep() {
            this.bingRef.instance.sleep();
        }
        /**
         * Updates this layer according to the current size of the projected map window.
         * @param projectedSize The size of the projected map window.
         */
        updateFromProjectedSize(projectedSize) {
            let offsetX, offsetY;
            if (this.props.wxrMode && this.props.wxrMode.get().mode === EWeatherRadar.HORIZONTAL) {
                const offsetSize = new Float64Array([projectedSize[0], projectedSize[1]]);
                const offset = this.props.mapProjection.getTargetProjectedOffset();
                offsetSize[0] += offset[0];
                offsetSize[1] += offset[1];
                this.size = this.getSize(offsetSize);
                offsetX = ((projectedSize[0] - this.size) / 2) + offset[0];
                offsetY = ((projectedSize[1] - this.size) / 2) + offset[1];
            }
            else {
                this.size = this.getSize(projectedSize);
                offsetX = (projectedSize[0] - this.size) / 2;
                offsetY = (projectedSize[1] - this.size) / 2;
            }
            this.wrapperStyle.set('left', `${offsetX}px`);
            this.wrapperStyle.set('top', `${offsetY}px`);
            this.wrapperStyle.set('width', `${this.size}px`);
            this.wrapperStyle.set('height', `${this.size}px`);
            this.resolution.set(this.size, this.size);
        }
        /**
         * Gets an appropriate size, in pixels, for this Bing layer given specific map projection window dimensions.
         * We get the length of the hypotenuse so that the map edges won't show when rotating.
         * @param projectedSize - the size of the projected map window.
         * @returns an appropriate size for this Bing layer.
         */
        getSize(projectedSize) {
            return Vec2Math.abs(projectedSize);
        }
        /** @inheritDoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            if (BitFlags.isAny(changeFlags, exports.MapProjectionChangeType.ProjectedSize | exports.MapProjectionChangeType.TargetProjected)) {
                this.updateFromProjectedSize(mapProjection.getProjectedSize());
            }
            if (this.bingRef.instance.isBound()) {
                this.needUpdate = true;
            }
        }
        /**
         * A callback which is called when the Bing component is bound.
         */
        onBingBound() {
            this.needUpdate = true;
        }
        /** @inheritDoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            if (!this.needUpdate) {
                return;
            }
            this.updatePositionRadius();
            this.needUpdate = false;
        }
        /**
         * Resets the underlying Bing component's img src attribute.
         */
        resetImgSrc() {
            this.bingRef.instance.resetImgSrc();
        }
        /**
         * Updates the Bing map center position and radius.
         */
        updatePositionRadius() {
            const center = this.props.mapProjection.getCenter();
            const radius = this.calculateDesiredRadius(this.props.mapProjection);
            this.bingRef.instance.setPositionRadius(new LatLong(center.lat, center.lon), radius);
            if (!this.props.wxrMode || (this.props.wxrMode && this.props.wxrMode.get().mode !== EWeatherRadar.HORIZONTAL)) {
                this.rotationTransform.set(this.props.mapProjection.getRotation(), 1e-3);
            }
            else {
                this.rotationTransform.set(0);
            }
            this.wrapperStyle.set('transform', this.rotationTransform.resolve());
        }
        /**
         * Gets the desired Bing map radius in meters given a map projection model.
         * @param mapProjection - a map projection model.
         * @returns the desired Bing map radius.
         */
        calculateDesiredRadius(mapProjection) {
            const scaleFactor = mapProjection.getScaleFactor();
            const pointScaleFactor = 1 / Math.cos(mapProjection.getCenter().lat * Avionics.Utils.DEG2RAD);
            const radiusGARad = this.size / (2 * scaleFactor * pointScaleFactor);
            return UnitType.GA_RADIAN.convertTo(radiusGARad, UnitType.METER);
        }
        /** @inheritDoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { style: this.wrapperStyle, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' },
                exports.FSComponent.buildComponent(BingComponent, { ref: this.bingRef, id: this.props.bingId, onBoundCallback: this.onBingBound.bind(this), resolution: this.resolution, mode: EBingMode.PLANE, earthColors: this.props.earthColors, earthColorsElevationRange: this.props.earthColorsElevationRange, reference: this.props.reference, wxrMode: this.props.wxrMode, wxrColors: this.props.wxrColors, isoLines: this.props.isoLines, delay: this.props.bingDelay, skipUnbindOnDestroy: this.props.bingSkipUnbindOnDestroy })));
        }
        /** @inheritDoc */
        destroy() {
            var _a;
            (_a = this.bingRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            super.destroy();
        }
    }

    /**
     * A layer which displays text which can be culled to avoid overlap.
     */
    class MapCullableTextLayer extends MapSyncedCanvasLayer {
        // eslint-disable-next-line jsdoc/require-jsdoc
        onUpdated(time, elapsed) {
            super.onUpdated(time, elapsed);
            this.props.manager.update(this.props.mapProjection);
            this.redrawLabels();
        }
        /**
         * Clears this layer's canvas and redraws the currently visible labels registered to this layer's text manager.
         */
        redrawLabels() {
            const labels = this.props.manager.visibleLabels;
            const display = this.display;
            display.clear();
            for (let i = labels.length - 1; i >= 0; i--) {
                labels[i].draw(display.context, this.props.mapProjection);
            }
        }
    }

    /**
     * A generic map layer which renders its children.
     */
    class MapGenericLayer extends MapLayer {
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            this.props.onVisibilityChanged && this.props.onVisibilityChanged(this, isVisible);
        }
        /** @inheritdoc */
        onAttached() {
            this.props.onAttached && this.props.onAttached(this);
        }
        /** @inheritdoc */
        onWake() {
            this.props.onWake && this.props.onWake(this);
        }
        /** @inheritdoc */
        onSleep() {
            this.props.onSleep && this.props.onSleep(this);
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.props.onMapProjectionChanged && this.props.onMapProjectionChanged(this, mapProjection, changeFlags);
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            this.props.onUpdated && this.props.onUpdated(this, time, elapsed);
        }
        /** @inheritdoc */
        onDetached() {
            this.props.onDetached && this.props.onDetached(this);
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }, this.props.children));
        }
    }

    /**
     * A map shared canvas sublayer that displays a ring (circle) with one or more labels.
     */
    class MapLabeledRingCanvasSubLayer extends MapSharedCanvasSubLayer {
        constructor() {
            super(...arguments);
            this.labelContainerRef = exports.FSComponent.createRef();
            this.center = new Float64Array(2);
            this.radius = 0;
            this.strokeWidth = 0;
            this.strokeStyle = '';
            this.strokeDash = [];
            this.outlineWidth = 0;
            this.outlineStyle = '';
            this.outlineDash = [];
            this.labels = [];
            this.needInvalidate = false;
        }
        /**
         * Gets the center position of this sublayer's ring, in pixels.
         * @returns the center position of this sublayer's ring.
         */
        getRingCenter() {
            return this.center;
        }
        /**
         * Gets the radius of this sublayer's ring, in pixels.
         * @returns the radius of this sublayer's ring.
         */
        getRingRadius() {
            return this.radius;
        }
        /**
         * Sets the center and radius of this sublayer's ring.
         * @param center The new center, in pixels.
         * @param radius The new radius, in pixels.
         */
        setRingPosition(center, radius) {
            if (Vec2Math.equals(this.center, center) && radius === this.radius) {
                return;
            }
            this.center.set(center);
            this.radius = radius;
            this.needInvalidate = true;
        }
        /**
         * Sets the styling for this sublayer's ring stroke. Any style that is not explicitly defined will be left unchanged.
         * @param width The new stroke width.
         * @param style The new stroke style.
         * @param dash The new stroke dash.
         */
        setRingStrokeStyles(width, style, dash) {
            this.strokeWidth = width !== null && width !== void 0 ? width : this.strokeWidth;
            this.strokeStyle = style !== null && style !== void 0 ? style : this.strokeStyle;
            this.strokeDash = dash !== null && dash !== void 0 ? dash : this.strokeDash;
            this.needInvalidate = true;
        }
        /**
         * Sets the styling for this sublayer's ring outline. Any style that is not explicitly defined will be left unchanged.
         * @param width The new outline width.
         * @param style The new outline style.
         * @param dash The new outline dash.
         */
        setRingOutlineStyles(width, style, dash) {
            this.outlineWidth = width !== null && width !== void 0 ? width : this.outlineWidth;
            this.outlineStyle = style !== null && style !== void 0 ? style : this.outlineStyle;
            this.outlineDash = dash !== null && dash !== void 0 ? dash : this.outlineDash;
            this.needInvalidate = true;
        }
        /**
         * Creates a ring label. Labels can only be created after this sublayer has been rendered.
         * @param content The content of the new label.
         * @returns The newly created ring label, or `null` if a label could not be created.
         */
        createLabel(content) {
            if (!this.labelContainerRef.instance) {
                return null;
            }
            const node = (exports.FSComponent.buildComponent(DefaultMapLabeledRingLabel, null, content));
            exports.FSComponent.render(node, this.labelContainerRef.instance);
            const label = node.instance;
            label.setRingPosition(this.center, this.radius);
            this.labels.push(label);
            return label;
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            this.needInvalidate = true;
            this.labelContainerRef.instance.style.display = isVisible ? 'block' : 'none';
        }
        /** @inheritdoc */
        onAttached() {
            this.needInvalidate = true;
        }
        /** @inheritdoc */
        shouldInvalidate() {
            return this.needInvalidate;
        }
        /** @inheritdoc */
        onUpdated() {
            if (this.display.isInvalidated) {
                this.needInvalidate = false;
                if (!this.isVisible()) {
                    return;
                }
                this.drawRing();
                this.updateLabelPositions();
            }
        }
        /**
         * Draws this layer's ring.
         */
        drawRing() {
            if (!this.isRingInView()) {
                return;
            }
            this.display.context.beginPath();
            this.display.context.arc(this.center[0], this.center[1], this.radius, 0, Math.PI * 2);
            if (this.outlineWidth > 0) {
                this.applyStrokeToContext(this.display.context, this.strokeWidth + this.outlineWidth * 2, this.outlineStyle, this.outlineDash);
            }
            if (this.strokeWidth > 0) {
                this.applyStrokeToContext(this.display.context, this.strokeWidth, this.strokeStyle, this.strokeDash);
            }
        }
        /**
         * Checks whether this sublayer's ring is in view.
         * @returns whether this sublayer's ring is in view.
         */
        isRingInView() {
            const centerX = this.center[0];
            const centerY = this.center[1];
            const innerHalfLength = this.radius / Math.SQRT2;
            const innerLeft = centerX - innerHalfLength;
            const innerRight = centerX + innerHalfLength;
            const innerTop = centerY - innerHalfLength;
            const innerBottom = centerY + innerHalfLength;
            const outerLeft = centerX - this.radius;
            const outerRight = centerX + this.radius;
            const outerTop = centerY - this.radius;
            const outerBottom = centerY + this.radius;
            const width = this.projection.getProjectedSize()[0];
            const height = this.projection.getProjectedSize()[1];
            if (innerLeft < 0 && innerRight > width && innerTop < 0 && innerBottom > height) {
                return false;
            }
            if (outerLeft > width || outerRight < 0 || outerTop > height || outerBottom < 0) {
                return false;
            }
            return true;
        }
        /**
         * Applies a stroke to a canvas rendering context.
         * @param context The canvas to which to apply a stroke.
         * @param lineWidth The stroke width.
         * @param strokeStyle The stroke style.
         * @param dash The stroke dash.
         */
        applyStrokeToContext(context, lineWidth, strokeStyle, dash) {
            context.lineWidth = lineWidth;
            context.strokeStyle = strokeStyle;
            context.setLineDash(dash);
            context.stroke();
        }
        /**
         * Updates the position of this sublayer's labels based on the position of the ring.
         */
        updateLabelPositions() {
            const len = this.labels.length;
            for (let i = 0; i < len; i++) {
                this.labels[i].setRingPosition(this.center, this.radius);
            }
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent("div", { ref: this.labelContainerRef, style: 'position: absolute; left: 0; top: 0; width: 100%; height: 100%;' }));
        }
        /** @inheritdoc */
        destroy() {
            for (const label of this.labels) {
                label.destroy();
            }
            super.destroy();
        }
    }

    /**
     * A map layer that displays a ring (circle) with one or more labels.
     */
    class MapLabeledRingLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.labelContainerRef = exports.FSComponent.createRef();
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.center = new Float64Array(2);
            this.radius = 0;
            this.strokeWidth = 0;
            this.strokeStyle = '';
            this.strokeDash = [];
            this.outlineWidth = 0;
            this.outlineStyle = '';
            this.outlineDash = [];
            this.needUpdateRingPosition = false;
            this.isInit = false;
            this.labels = [];
        }
        /**
         * Gets the center position of this layer's ring, in pixels.
         * @returns the center position of this layer's ring.
         */
        getRingCenter() {
            return this.center;
        }
        /**
         * Gets the radius of this layer's ring, in pixels.
         * @returns the radius of this layer's ring.
         */
        getRingRadius() {
            return this.radius;
        }
        /**
         * Sets the center and radius of this layer's ring.
         * @param center The new center, in pixels.
         * @param radius The new radius, in pixels.
         */
        setRingPosition(center, radius) {
            if (Vec2Math.equals(this.center, center) && radius === this.radius) {
                return;
            }
            this.center.set(center);
            this.radius = radius;
            this.needUpdateRingPosition = true;
        }
        /**
         * Sets the styling for this layer's ring stroke. Any style that is not explicitly defined will be left unchanged.
         * @param width The new stroke width.
         * @param style The new stroke style.
         * @param dash The new stroke dash.
         */
        setRingStrokeStyles(width, style, dash) {
            this.strokeWidth = width !== null && width !== void 0 ? width : this.strokeWidth;
            this.strokeStyle = style !== null && style !== void 0 ? style : this.strokeStyle;
            this.strokeDash = dash !== null && dash !== void 0 ? dash : this.strokeDash;
            this.needUpdateRingPosition = true;
        }
        /**
         * Sets the styling for this layer's ring outline. Any style that is not explicitly defined will be left unchanged.
         * @param width The new outline width.
         * @param style The new outline style.
         * @param dash The new outline dash.
         */
        setRingOutlineStyles(width, style, dash) {
            this.outlineWidth = width !== null && width !== void 0 ? width : this.outlineWidth;
            this.outlineStyle = style !== null && style !== void 0 ? style : this.outlineStyle;
            this.outlineDash = dash !== null && dash !== void 0 ? dash : this.outlineDash;
            this.needUpdateRingPosition = true;
        }
        /**
         * Creates a ring label. Labels can only be created after this layer has been rendered.
         * @param content The content of the new label.
         * @returns the newly created ring label, or null if a label could not be created.
         */
        createLabel(content) {
            if (!this.labelContainerRef.instance) {
                return null;
            }
            const node = (exports.FSComponent.buildComponent(DefaultMapLabeledRingLabel, null, content));
            exports.FSComponent.render(node, this.labelContainerRef.instance);
            const label = node.instance;
            label.setRingPosition(this.center, this.radius);
            this.labels.push(label);
            return label;
        }
        /** @inheritdoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            if (this.isInit) {
                this.updateFromVisibility();
            }
        }
        /** @inheritdoc */
        onAttached() {
            this.canvasLayerRef.instance.onAttached();
            this.isInit = true;
            this.updateFromVisibility();
            this.needUpdateRingPosition = true;
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                // resizing the map will cause the canvas layer to clear itself, so we need to force a redraw.
                this.needUpdateRingPosition = true;
            }
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            if (!this.isVisible()) {
                return;
            }
            if (this.needUpdateRingPosition) {
                this.updateRingPosition();
                this.needUpdateRingPosition = false;
            }
            this.canvasLayerRef.instance.onUpdated(time, elapsed);
        }
        /**
         * Updates this layer according to its current visibility.
         */
        updateFromVisibility() {
            const isVisible = this.isVisible();
            this.canvasLayerRef.instance.setVisible(isVisible);
            this.labelContainerRef.instance.style.display = isVisible ? 'block' : 'none';
        }
        /**
         * Updates the position of this layer's ring.
         */
        updateRingPosition() {
            this.drawRing();
            this.updateLabelPositions();
        }
        /**
         * Draws this layer's ring to canvas.
         */
        drawRing() {
            const canvasDisplay = this.canvasLayerRef.instance.display;
            canvasDisplay.clear();
            if (!this.isRingInView()) {
                return;
            }
            canvasDisplay.context.beginPath();
            canvasDisplay.context.arc(this.center[0], this.center[1], this.radius, 0, Math.PI * 2);
            if (this.outlineWidth > 0) {
                this.applyStrokeToContext(canvasDisplay.context, this.strokeWidth + this.outlineWidth * 2, this.outlineStyle, this.outlineDash);
            }
            if (this.strokeWidth > 0) {
                this.applyStrokeToContext(canvasDisplay.context, this.strokeWidth, this.strokeStyle, this.strokeDash);
            }
        }
        /**
         * Checks whether this layer's ring is in view.
         * @returns whether this layer's ring is in view.
         */
        isRingInView() {
            const centerX = this.center[0];
            const centerY = this.center[1];
            const innerHalfLength = this.radius / Math.SQRT2;
            const innerLeft = centerX - innerHalfLength;
            const innerRight = centerX + innerHalfLength;
            const innerTop = centerY - innerHalfLength;
            const innerBottom = centerY + innerHalfLength;
            const outerLeft = centerX - this.radius;
            const outerRight = centerX + this.radius;
            const outerTop = centerY - this.radius;
            const outerBottom = centerY + this.radius;
            const width = this.props.mapProjection.getProjectedSize()[0];
            const height = this.props.mapProjection.getProjectedSize()[1];
            if (innerLeft < 0 && innerRight > width && innerTop < 0 && innerBottom > height) {
                return false;
            }
            if (outerLeft > width || outerRight < 0 || outerTop > height || outerBottom < 0) {
                return false;
            }
            return true;
        }
        /**
         * Applies a stroke to a canvas rendering context.
         * @param context The canvas to which to apply a stroke.
         * @param lineWidth The stroke width.
         * @param strokeStyle The stroke style.
         * @param dash The stroke dash.
         */
        applyStrokeToContext(context, lineWidth, strokeStyle, dash) {
            context.lineWidth = lineWidth;
            context.strokeStyle = strokeStyle;
            context.setLineDash(dash);
            context.stroke();
        }
        /**
         * Updates the position of this layer's labels based on the position of the ring.
         */
        updateLabelPositions() {
            const len = this.labels.length;
            for (let i = 0; i < len; i++) {
                this.labels[i].setRingPosition(this.center, this.radius);
            }
        }
        /** @inheritdoc */
        render() {
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null,
                exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection }),
                exports.FSComponent.buildComponent("div", { ref: this.labelContainerRef, style: 'position: absolute; left: 0; top: 0; width: 100%; height: 100%;' })));
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.canvasLayerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            for (const label of this.labels) {
                label.destroy();
            }
            super.destroy();
        }
    }

    /**
     * A map layer that draws a line between two points. The line is drawn in projected coordinate space, so it will always
     * be straight on the projected map.
     */
    class MapLineLayer extends MapSyncedCanvasLayer {
        constructor() {
            var _a, _b, _c, _d, _e, _f;
            super(...arguments);
            this.strokeWidth = (_a = this.props.strokeWidth) !== null && _a !== void 0 ? _a : MapLineLayer.DEFAULT_STROKE_WIDTH;
            this.strokeStyle = (_b = this.props.strokeStyle) !== null && _b !== void 0 ? _b : MapLineLayer.DEFAULT_STROKE_STYLE;
            this.strokeDash = (_c = this.props.strokeDash) !== null && _c !== void 0 ? _c : MapLineLayer.DEFAULT_STROKE_DASH;
            this.outlineWidth = (_d = this.props.outlineWidth) !== null && _d !== void 0 ? _d : MapLineLayer.DEFAULT_OUTLINE_WIDTH;
            this.outlineStyle = (_e = this.props.outlineStyle) !== null && _e !== void 0 ? _e : MapLineLayer.DEFAULT_OUTLINE_STYLE;
            this.outlineDash = (_f = this.props.outlineDash) !== null && _f !== void 0 ? _f : MapLineLayer.DEFAULT_OUTLINE_DASH;
            this.vec = new Float64Array([0, 0]);
            this.isUpdateScheduled = false;
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.props.start.sub(() => { this.scheduleUpdate(); });
            this.props.end.sub(() => { this.scheduleUpdate(); });
            this.scheduleUpdate();
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            super.onMapProjectionChanged(mapProjection, changeFlags);
            this.scheduleUpdate();
        }
        /**
         * Schedules the layer for a draw update.
         */
        scheduleUpdate() {
            this.isUpdateScheduled = true;
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            super.onUpdated(time, elapsed);
            if (this.isUpdateScheduled) {
                this.display.clear();
                const start = this.props.start.get();
                const end = this.props.end.get();
                if (start !== null && end !== null) {
                    const [x1, y1] = start instanceof Float64Array ? start : this.props.mapProjection.project(start, this.vec);
                    const [x2, y2] = end instanceof Float64Array ? end : this.props.mapProjection.project(end, this.vec);
                    this.drawLine(x1, y1, x2, y2);
                }
                this.isUpdateScheduled = false;
            }
        }
        /**
         * Draws this layer's line.
         * @param x1 The x coordinate of the start of the line.
         * @param y1 The y coordinate of the start of the line.
         * @param x2 The x coordinate of the end of the line.
         * @param y2 The y coordinate of the end of the line.
         */
        drawLine(x1, y1, x2, y2) {
            const context = this.display.context;
            context.beginPath();
            context.moveTo(x1, y1);
            context.lineTo(x2, y2);
            if (this.outlineWidth > 0) {
                this.stroke(context, this.strokeWidth + this.outlineWidth * 2, this.outlineStyle, this.outlineDash);
            }
            if (this.strokeWidth > 0) {
                this.stroke(context, this.strokeWidth, this.strokeStyle, this.strokeDash);
            }
        }
        /**
         * Applies a stroke to a canvas rendering context.
         * @param context A canvas rendering context.
         * @param width The width of the stroke, in pixels.
         * @param style The style of the stroke.
         * @param dash The dash array of the stroke.
         */
        stroke(context, width, style, dash) {
            context.lineWidth = width;
            context.strokeStyle = style;
            context.setLineDash(dash);
            context.stroke();
        }
    }
    MapLineLayer.DEFAULT_STROKE_WIDTH = 2; // px
    MapLineLayer.DEFAULT_STROKE_STYLE = 'white';
    MapLineLayer.DEFAULT_STROKE_DASH = [];
    MapLineLayer.DEFAULT_OUTLINE_WIDTH = 0; // px
    MapLineLayer.DEFAULT_OUTLINE_STYLE = 'black';
    MapLineLayer.DEFAULT_OUTLINE_DASH = [];

    /**
     * An abstract implementation of a map layer which displays waypoints (airports, navaids, and intersections) within a
     * search radius.
     */
    class MapNearestWaypointsLayer extends MapLayer {
        constructor() {
            var _a;
            super(...arguments);
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.searchDebounceDelay = (_a = this.props.searchDebounceDelay) !== null && _a !== void 0 ? _a : 500;
            this.facLoader = new FacilityLoader(FacilityRepository.getRepository(this.props.bus), this.onFacilityLoaderInitialized.bind(this));
            this.searchRadius = 0;
            this.searchMargin = 0;
            this.userFacilityHasChanged = false;
            /** A set of the ICAOs of all waypoints that should be rendered. */
            this.icaosToRender = new Set();
            /** A map of rendered waypoints from their ICAOs. */
            this.cachedRenderedWaypoints = new Map();
            this.isInit = false;
            this.facilityRepoSubs = [];
        }
        /**
         * A callback called when the facility loaded finishes initialization.
         */
        onFacilityLoaderInitialized() {
            Promise.all([
                this.facLoader.startNearestSearchSessionWithIcaoStructs(exports.FacilitySearchType.Airport),
                this.facLoader.startNearestSearchSessionWithIcaoStructs(exports.FacilitySearchType.Vor),
                this.facLoader.startNearestSearchSessionWithIcaoStructs(exports.FacilitySearchType.Ndb),
                this.facLoader.startNearestSearchSessionWithIcaoStructs(exports.FacilitySearchType.Intersection),
                this.facLoader.startNearestSearchSessionWithIcaoStructs(exports.FacilitySearchType.User)
            ]).then((value) => {
                const [airportSession, vorSession, ndbSession, intSession, userSession] = value;
                this.onSessionsStarted(airportSession, vorSession, ndbSession, intSession, userSession);
            });
        }
        /**
         * A callback called when the nearest facility search sessions have been started.
         * @param airportSession The airport search session.
         * @param vorSession The VOR search session.
         * @param ndbSession The NDB search session.
         * @param intSession The intersection search session.
         * @param userSession The user facility search session.
         */
        onSessionsStarted(airportSession, vorSession, ndbSession, intSession, userSession) {
            const callback = this.processSearchResults.bind(this);
            this.facilitySearches = {
                [exports.FacilitySearchType.Airport]: new MapNearestWaypointsLayerSearch(airportSession, callback),
                [exports.FacilitySearchType.Vor]: new MapNearestWaypointsLayerSearch(vorSession, callback),
                [exports.FacilitySearchType.Ndb]: new MapNearestWaypointsLayerSearch(ndbSession, callback),
                [exports.FacilitySearchType.Intersection]: new MapNearestWaypointsLayerSearch(intSession, callback),
                [exports.FacilitySearchType.User]: new MapNearestWaypointsLayerSearch(userSession, callback)
            };
            const sub = this.props.bus.getSubscriber();
            // Watch for changes to user facilities so that we can trigger search refreshes to ensure that the layer does not
            // display outdated user waypoints.
            this.facilityRepoSubs.push(sub.on('facility_added').handle(fac => {
                if (ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.USR)) {
                    this.userFacilityHasChanged = true;
                }
            }), sub.on('facility_changed').handle(fac => {
                if (ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.USR)) {
                    this.userFacilityHasChanged = true;
                }
            }), sub.on('facility_removed').handle(fac => {
                if (ICAO.isValueFacility(fac.icaoStruct, exports.FacilityType.USR)) {
                    this.userFacilityHasChanged = true;
                }
            }));
            this.props.onSessionsStarted && this.props.onSessionsStarted(airportSession, vorSession, ndbSession, intSession, userSession);
            if (this.isInit) {
                this._tryRefreshAllSearches(this.getSearchCenter(), this.searchRadius);
            }
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.canvasLayerRef.instance.onAttached();
            this.doInit();
            this.isInit = true;
            this._tryRefreshAllSearches(this.getSearchCenter(), this.searchRadius);
        }
        /**
         * Initializes this layer.
         */
        doInit() {
            this.initWaypointRenderer();
            this.updateSearchRadius();
        }
        /**
         * Gets the search center for the waypoint searches on this layer.
         * @returns The waypoint search center geo point.
         */
        getSearchCenter() {
            return this.props.getSearchCenter ? this.props.getSearchCenter(this.props.mapProjection) : this.props.mapProjection.getCenter();
        }
        /**
         * Initializes this layer's waypoint renderer.
         */
        initWaypointRenderer() {
            this.props.initRenderer && this.props.initRenderer(this.props.waypointRenderer, this.canvasLayerRef.instance);
        }
        /** Forces a refresh of all the waypoints. */
        refreshWaypoints() {
            this.tryRefreshAllSearches(undefined, undefined, true);
            this.cachedRenderedWaypoints.forEach(w => {
                this.props.deregisterWaypoint(w, this.props.waypointRenderer);
            });
            this.cachedRenderedWaypoints.forEach(w => {
                this.props.registerWaypoint(w, this.props.waypointRenderer);
            });
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            if (BitFlags.isAny(changeFlags, exports.MapProjectionChangeType.Range | exports.MapProjectionChangeType.RangeEndpoints | exports.MapProjectionChangeType.ProjectedSize)) {
                this.updateSearchRadius();
                this._tryRefreshAllSearches(this.getSearchCenter(), this.searchRadius);
            }
            else if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.Center)) {
                this._tryRefreshAllSearches(this.getSearchCenter(), this.searchRadius);
            }
        }
        /**
         * Updates the desired nearest facility search radius based on the current map projection.
         */
        updateSearchRadius() {
            let mapHalfDiagRange = Vec2Math.abs(this.props.mapProjection.getProjectedSize()) * this.props.mapProjection.getProjectedResolution() / 2;
            //Limit lower end of radius so that even at high zooms the surrounding area waypoints are captured.
            mapHalfDiagRange = Math.max(mapHalfDiagRange, UnitType.NMILE.convertTo(5, UnitType.GA_RADIAN));
            this.searchRadius = mapHalfDiagRange * MapNearestWaypointsLayer.SEARCH_RADIUS_OVERDRAW_FACTOR;
            this.searchMargin = mapHalfDiagRange * (MapNearestWaypointsLayer.SEARCH_RADIUS_OVERDRAW_FACTOR - 1);
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            var _a;
            // If a user facility was added, changed, or removed, schedule a user waypoint search refresh so that we always
            // have the latest user facility data.
            if (this.userFacilityHasChanged) {
                const search = (_a = this.facilitySearches) === null || _a === void 0 ? void 0 : _a[exports.FacilitySearchType.User];
                if (search !== undefined) {
                    this.userFacilityHasChanged = false;
                    this.scheduleSearchRefresh(exports.FacilitySearchType.User, search, this.getSearchCenter(), this.searchRadius);
                }
            }
            this.updateSearches(elapsed);
        }
        /**
         * Updates this layer's facility searches.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        updateSearches(elapsed) {
            if (!this.facilitySearches) {
                return;
            }
            this.facilitySearches[exports.FacilitySearchType.Airport].update(elapsed);
            this.facilitySearches[exports.FacilitySearchType.Vor].update(elapsed);
            this.facilitySearches[exports.FacilitySearchType.Ndb].update(elapsed);
            this.facilitySearches[exports.FacilitySearchType.Intersection].update(elapsed);
            this.facilitySearches[exports.FacilitySearchType.User].update(elapsed);
        }
        /**
         * Attempts to refresh all of the nearest facility searches. Searches will only be refreshed if the desired search
         * radius is different from the last refreshed search radius or the desired search center is outside of the margin
         * of the last refreshed search center.
         * @param center The center of the search area. Defaults to this layer's automatically calculated search center.
         * @param radius The radius of the search area, in great-arc radians. Defaults to this layer's automatically
         * calculated search radius.
         * @param force Whether to force a refresh of all waypoints. Defaults to false.
         */
        tryRefreshAllSearches(center, radius, force) {
            center !== null && center !== void 0 ? center : (center = this.getSearchCenter());
            radius !== null && radius !== void 0 ? radius : (radius = this.searchRadius);
            this._tryRefreshAllSearches(center, radius, force);
        }
        /**
         * Attempts to refresh a nearest search. The search will only be refreshed if the desired search radius is different
         * from the last refreshed search radius or the desired search center is outside of the margin of the last refreshed
         * search center.
         * @param type The type of nearest search to refresh.
         * @param center The center of the search area. Defaults to this layer's automatically calculated search center.
         * @param radius The radius of the search area, in great-arc radians. Defaults to this layer's automatically
         * calculated search radius.
         * @param force Whether to force a refresh of all waypoints. Defaults to false.
         */
        tryRefreshSearch(type, center, radius, force) {
            center !== null && center !== void 0 ? center : (center = this.getSearchCenter());
            radius !== null && radius !== void 0 ? radius : (radius = this.searchRadius);
            this._tryRefreshSearch(type, center, radius, force);
        }
        /**
         * Attempts to refresh all of the nearest facility searches.
         * @param center The center of the search area.
         * @param radius The radius of the search area, in great-arc radians.
         * @param force Whether to force a refresh of all waypoints. Defaults to false.
         */
        _tryRefreshAllSearches(center, radius, force) {
            this._tryRefreshSearch(exports.FacilitySearchType.Airport, center, radius, force);
            this._tryRefreshSearch(exports.FacilitySearchType.Vor, center, radius, force);
            this._tryRefreshSearch(exports.FacilitySearchType.Ndb, center, radius, force);
            this._tryRefreshSearch(exports.FacilitySearchType.Intersection, center, radius, force);
            this._tryRefreshSearch(exports.FacilitySearchType.User, center, radius, force);
        }
        /**
         * Attempts to refresh a nearest search. The search will only be refreshed if `this.shouldRefreshSearch()` returns
         * true and and the desired search radius is different from the last refreshed search radius or the desired search
         * center is outside of the margin of the last refreshed search center.
         * @param type The type of nearest search to refresh.
         * @param center The center of the search area.
         * @param radius The radius of the search area, in great-arc radians.
         * @param force Whether to force a refresh of all waypoints. Defaults to false.
         */
        _tryRefreshSearch(type, center, radius, force) {
            const search = this.facilitySearches && this.facilitySearches[type];
            if (!search || (!force && !this.shouldRefreshSearch(type, center, radius))) {
                return;
            }
            const radiusLimit = this.props.searchRadiusLimit ? this.props.searchRadiusLimit(type, center, radius) : undefined;
            if (radiusLimit !== undefined && isFinite(radiusLimit)) {
                radius = Math.min(radius, Math.max(0, radiusLimit));
            }
            if (force || search.lastRadius !== radius || search.lastCenter.distance(center) >= this.searchMargin) {
                this.scheduleSearchRefresh(type, search, center, radius);
            }
        }
        /**
         * Checks whether one of this layer's searches should be refreshed.
         * @param type The type of nearest search to refresh.
         * @param center The center of the search area.
         * @param radius The radius of the search area, in great-arc radians.
         * @returns Whether the search should be refreshed.
         */
        shouldRefreshSearch(type, center, radius) {
            return this.props.shouldRefreshSearch ? this.props.shouldRefreshSearch(type, center, radius) : true;
        }
        /**
         * Schedules a refresh of this one of this layer's searches.
         * @param type The type of nearest search to refresh.
         * @param search The search to refresh.
         * @param center The center of the search area.
         * @param radius The radius of the search area, in great-arc radians.
         */
        scheduleSearchRefresh(type, search, center, radius) {
            const itemLimit = this.props.searchItemLimit ? this.props.searchItemLimit(type, center, radius) : 100;
            search.scheduleRefresh(center, radius, itemLimit, this.searchDebounceDelay);
        }
        /**
         * Processes nearest facility search results. New facilities are registered, while removed facilities are
         * deregistered.
         * @param results Nearest facility search results.
         */
        processSearchResults(results) {
            if (!results) {
                return;
            }
            const numAdded = results.added.length;
            for (let i = 0; i < numAdded; i++) {
                const icao = results.added[i];
                if (ICAO.isValueEmpty(icao)) {
                    continue;
                }
                this.registerIcao(icao);
            }
            const numRemoved = results.removed.length;
            for (let i = 0; i < numRemoved; i++) {
                const icao = results.removed[i];
                if (ICAO.isValueEmpty(icao)) {
                    continue;
                }
                this.deregisterIcao(icao);
            }
        }
        /**
         * Registers an ICAO string with this layer. Once an ICAO is registered, its corresponding facility is drawn to this
         * layer using a waypoint renderer.
         * @param icao The ICAO string to register.
         */
        async registerIcao(icao) {
            const uid = ICAO.getUid(icao);
            this.icaosToRender.add(uid);
            try {
                const facility = await this.facLoader.tryGetFacility(ICAO.getFacilityTypeFromValue(icao), icao);
                if (facility) {
                    if (!this.icaosToRender.has(uid)) {
                        return;
                    }
                    this.registerWaypointWithRenderer(this.props.waypointRenderer, facility);
                }
            }
            catch (_a) {
                // noop
            }
        }
        /**
         * Registers a facility with this layer's waypoint renderer.
         * @param renderer This layer's waypoint renderer.
         * @param facility The facility to register.
         */
        registerWaypointWithRenderer(renderer, facility) {
            const waypoint = this.props.waypointForFacility(facility);
            this.cachedRenderedWaypoints.set(ICAO.getUid(facility.icaoStruct), waypoint);
            this.props.registerWaypoint(waypoint, renderer);
        }
        /**
         * Deregisters an ICAO string from this layer.
         * @param icao The ICAO string to deregister.
         */
        async deregisterIcao(icao) {
            const uid = ICAO.getUid(icao);
            this.icaosToRender.delete(uid);
            let facility = null;
            try {
                facility = await this.facLoader.tryGetFacility(ICAO.getFacilityTypeFromValue(icao), icao);
            }
            catch (_a) {
                // noop
            }
            if (this.icaosToRender.has(uid)) {
                return;
            }
            if (facility) {
                this.deregisterWaypointWithRenderer(this.props.waypointRenderer, facility);
            }
            else {
                // If we can't find the facility from the ICAO, it could be that the facility has been removed, in which case
                // we grab the cached waypoint (the waypoint that was most recently registered with the renderer under the
                // removed ICAO) and deregister it.
                const cachedWaypoint = this.cachedRenderedWaypoints.get(uid);
                if (cachedWaypoint !== undefined) {
                    this.cachedRenderedWaypoints.delete(uid);
                    this.props.deregisterWaypoint(cachedWaypoint, this.props.waypointRenderer);
                }
            }
        }
        /**
         * Deregisters a facility from this layer's waypoint renderer.
         * @param renderer This layer's waypoint renderer.
         * @param facility The facility to deregister.
         */
        deregisterWaypointWithRenderer(renderer, facility) {
            const waypoint = this.props.waypointForFacility(facility);
            this.cachedRenderedWaypoints.delete(ICAO.getUid(facility.icaoStruct));
            this.props.deregisterWaypoint(waypoint, renderer);
        }
        /** @inheritdoc */
        setVisible(val) {
            super.setVisible(val);
            this.canvasLayerRef.instance.setVisible(val);
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }));
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.canvasLayerRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            this.facilityRepoSubs.forEach(sub => { sub.destroy(); });
            super.destroy();
        }
    }
    MapNearestWaypointsLayer.SEARCH_RADIUS_OVERDRAW_FACTOR = Math.SQRT2;
    /**
     * A nearest facility search for MapAbstractNearestWaypointsLayer.
     */
    class MapNearestWaypointsLayerSearch {
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The center of this search's last refresh.
         */
        get lastCenter() {
            return this._lastCenter.readonly;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The radius of this search's last refresh, in great-arc radians.
         */
        get lastRadius() {
            return this._lastRadius;
        }
        /**
         * Constructor.
         * @param session The session used by this search.
         * @param refreshCallback A callback which is called every time the search refreshes.
         */
        constructor(session, refreshCallback) {
            this.session = session;
            this.refreshCallback = refreshCallback;
            this._lastCenter = new GeoPoint(0, 0);
            this._lastRadius = 0;
            this.maxItemCount = 0;
            this.refreshDebounceTimer = 0;
            this.isRefreshScheduled = false;
        }
        /**
         * Schedules a refresh of this search.  If a refresh was previously scheduled but not yet executed, this new
         * scheduled refresh will replace the old one.
         * @param center The center of the search area.
         * @param radius The radius of the search area, in great-arc radians.
         * @param maxItemCount The maximum number of results returned by the refresh.
         * @param delay The delay, in milliseconds, before the refresh is executed.
         */
        scheduleRefresh(center, radius, maxItemCount, delay) {
            this._lastCenter.set(center);
            this._lastRadius = radius;
            this.maxItemCount = maxItemCount;
            if (!this.isRefreshScheduled) {
                this.refreshDebounceTimer = delay;
                this.isRefreshScheduled = true;
            }
        }
        /**
         * Updates this search. Executes any pending refreshes if their delay timers have expired.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        update(elapsed) {
            if (!this.isRefreshScheduled) {
                return;
            }
            this.refreshDebounceTimer = Math.max(0, this.refreshDebounceTimer - elapsed);
            if (this.refreshDebounceTimer === 0) {
                this.refresh();
                this.isRefreshScheduled = false;
            }
        }
        /**
         * Refreshes this search.
         * @returns a Promise which is fulfilled when the refresh completes.
         */
        async refresh() {
            const results = await this.session.searchNearest(this._lastCenter.lat, this._lastCenter.lon, UnitType.GA_RADIAN.convertTo(this._lastRadius, UnitType.METER), this.maxItemCount);
            this.refreshCallback(results);
        }
    }

    /* eslint-disable @typescript-eslint/no-non-null-assertion */
    /**
     * A layer which draws an own airplane icon. The icon is positioned at the projected location of the airplane and is
     * rotated to match the airplane's heading.
     */
    class MapOwnAirplaneLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.imageFilePath = SubscribableUtils.isSubscribable(this.props.imageFilePath)
                ? this.props.imageFilePath.map(SubscribableMapFunctions.identity())
                : this.props.imageFilePath;
            this.style = ObjectSubject.create({
                display: '',
                position: 'absolute',
                left: '0px',
                top: '0px',
                width: '0px',
                height: '0px',
                transform: 'translate3d(0, 0, 0) rotate(0deg)',
                'transform-origin': '50% 50%'
            });
            this.ownAirplanePropsModule = this.props.model.getModule('ownAirplaneProps');
            this.ownAirplaneIconModule = this.props.model.getModule('ownAirplaneIcon');
            this.iconSize = SubscribableUtils.toSubscribable(this.props.iconSize, true);
            this.iconAnchor = SubscribableUtils.toSubscribable(this.props.iconAnchor, true);
            this.iconOffset = Vec2Math.create();
            this.visibilityBounds = VecNMath.create(4);
            this.iconTransform = CssTransformBuilder.concat(CssTransformBuilder.translate3d('px'), CssTransformBuilder.rotate('deg'));
            this.isGsAboveTrackThreshold = this.ownAirplanePropsModule.groundSpeed.map(gs => gs.asUnit(UnitType.KNOT) >= 5).pause();
            this.showIcon = true;
            this.isInsideVisibilityBounds = true;
            this.planeRotation = 0;
            this.needUpdateVisibility = false;
            this.needUpdatePositionRotation = false;
        }
        /** @inheritdoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            this.needUpdateVisibility = true;
            this.needUpdatePositionRotation = this.showIcon = isVisible && this.ownAirplaneIconModule.show.get();
        }
        /** @inheritdoc */
        onAttached() {
            this.showSub = this.ownAirplaneIconModule.show.sub(show => {
                this.needUpdateVisibility = true;
                this.needUpdatePositionRotation = this.showIcon = show && this.isVisible();
            });
            this.positionSub = this.ownAirplanePropsModule.position.sub(() => {
                this.needUpdatePositionRotation = this.showIcon;
            });
            this.headingSub = this.ownAirplanePropsModule.hdgTrue.sub(hdg => {
                this.planeRotation = hdg;
                this.needUpdatePositionRotation = this.showIcon;
            }, false, true);
            this.trackSub = this.ownAirplanePropsModule.trackTrue.sub(track => {
                this.planeRotation = track;
                this.needUpdatePositionRotation = this.showIcon;
            }, false, true);
            this.trackThresholdSub = this.isGsAboveTrackThreshold.sub(isAboveThreshold => {
                if (isAboveThreshold) {
                    this.headingSub.pause();
                    this.trackSub.resume(true);
                }
                else {
                    this.trackSub.pause();
                    this.headingSub.resume(true);
                }
            }, false, true);
            this.iconSizeSub = this.iconSize.sub(size => {
                this.style.set('width', `${size}px`);
                this.style.set('height', `${size}px`);
                this.updateOffset();
            }, true);
            this.iconAnchorSub = this.iconAnchor.sub(() => {
                this.updateOffset();
            });
            this.orientationSub = this.ownAirplaneIconModule.orientation.sub(orientation => {
                switch (orientation) {
                    case exports.MapOwnAirplaneIconOrientation.HeadingUp:
                        this.isGsAboveTrackThreshold.pause();
                        this.trackThresholdSub.pause();
                        this.trackSub.pause();
                        this.headingSub.resume(true);
                        break;
                    case exports.MapOwnAirplaneIconOrientation.TrackUp:
                        this.headingSub.pause();
                        this.trackSub.pause();
                        this.isGsAboveTrackThreshold.resume();
                        this.trackThresholdSub.resume(true);
                        break;
                    default:
                        this.needUpdatePositionRotation = this.showIcon;
                        this.isGsAboveTrackThreshold.pause();
                        this.trackThresholdSub.pause();
                        this.headingSub.pause();
                        this.trackSub.pause();
                        this.planeRotation = 0;
                }
            }, true);
            this.needUpdateVisibility = true;
            this.needUpdatePositionRotation = true;
        }
        /**
         * Updates the icon's offset from the projected position of the airplane.
         */
        updateOffset() {
            const anchor = this.iconAnchor.get();
            this.iconOffset.set(anchor);
            Vec2Math.multScalar(this.iconOffset, -this.iconSize.get(), this.iconOffset);
            this.style.set('left', `${this.iconOffset[0]}px`);
            this.style.set('top', `${this.iconOffset[1]}px`);
            this.style.set('transform-origin', `${anchor[0] * 100}% ${anchor[1] * 100}%`);
            this.updateVisibilityBounds();
        }
        /**
         * Updates the boundaries within the map's projected window that define a region such that if the airplane's
         * projected position falls outside of it, the icon is not visible and therefore does not need to be updated.
         */
        updateVisibilityBounds() {
            const size = this.iconSize.get();
            // Find the maximum possible protrusion of the icon from its anchor point, defined as the distance from the
            // anchor point to the farthest point within the bounds of the icon. This farthest point is always one of the
            // four corners of the icon.
            const maxProtrusion = Math.max(Math.hypot(this.iconOffset[0], this.iconOffset[1]), // top left corner
            Math.hypot(this.iconOffset[0] + size, this.iconOffset[1]), // top right corner
            Math.hypot(this.iconOffset[0] + size, this.iconOffset[1] + size), // bottom right corner
            Math.hypot(this.iconOffset[0], this.iconOffset[1] + size));
            const boundsOffset = maxProtrusion + 50; // Add some additional buffer
            const projectedSize = this.props.mapProjection.getProjectedSize();
            this.visibilityBounds[0] = -boundsOffset;
            this.visibilityBounds[1] = -boundsOffset;
            this.visibilityBounds[2] = projectedSize[0] + boundsOffset;
            this.visibilityBounds[3] = projectedSize[1] + boundsOffset;
            this.needUpdatePositionRotation = this.showIcon;
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.updateVisibilityBounds();
            }
            this.needUpdatePositionRotation = this.showIcon;
        }
        /** @inheritdoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            if (this.needUpdatePositionRotation) {
                this.updateIconPositionRotation();
                this.needUpdatePositionRotation = false;
                this.needUpdateVisibility = false;
            }
            else if (this.needUpdateVisibility) {
                this.updateIconVisibility();
                this.needUpdateVisibility = false;
            }
        }
        /**
         * Updates the airplane icon's visibility.
         */
        updateIconVisibility() {
            this.style.set('display', this.isInsideVisibilityBounds && this.showIcon ? '' : 'none');
        }
        /**
         * Updates the airplane icon's projected position and rotation.
         */
        updateIconPositionRotation() {
            const projected = this.props.mapProjection.project(this.ownAirplanePropsModule.position.get(), MapOwnAirplaneLayer.vec2Cache[0]);
            this.isInsideVisibilityBounds = this.props.mapProjection.isInProjectedBounds(projected, this.visibilityBounds);
            // If the projected position of the icon is far enough out of bounds that the icon is not visible, do not bother to
            // update the icon.
            if (this.isInsideVisibilityBounds) {
                let rotation;
                switch (this.ownAirplaneIconModule.orientation.get()) {
                    case exports.MapOwnAirplaneIconOrientation.HeadingUp:
                    case exports.MapOwnAirplaneIconOrientation.TrackUp:
                        rotation = this.planeRotation + this.props.mapProjection.getRotation() * Avionics.Utils.RAD2DEG;
                        break;
                    default:
                        rotation = 0;
                }
                this.iconTransform.getChild(0).set(projected[0], projected[1], 0, 0.1);
                this.iconTransform.getChild(1).set(rotation, 0.1);
                this.style.set('transform', this.iconTransform.resolve());
            }
            this.updateIconVisibility();
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("img", { src: this.imageFilePath, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '', style: this.style }));
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            if (SubscribableUtils.isSubscribable(this.imageFilePath)) {
                this.imageFilePath.destroy();
            }
            this.isGsAboveTrackThreshold.destroy();
            (_a = this.showSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.positionSub) === null || _b === void 0 ? void 0 : _b.destroy();
            (_c = this.headingSub) === null || _c === void 0 ? void 0 : _c.destroy();
            (_d = this.trackSub) === null || _d === void 0 ? void 0 : _d.destroy();
            (_e = this.trackThresholdSub) === null || _e === void 0 ? void 0 : _e.destroy();
            (_f = this.iconSizeSub) === null || _f === void 0 ? void 0 : _f.destroy();
            (_g = this.iconAnchorSub) === null || _g === void 0 ? void 0 : _g.destroy();
            (_h = this.orientationSub) === null || _h === void 0 ? void 0 : _h.destroy();
            super.destroy();
        }
    }
    MapOwnAirplaneLayer.vec2Cache = [Vec2Math.create()];

    /**
     * A map layer containing a single canvas whose image can be cached and transformed as the map projection changes and
     * can be shared amongst multiple sublayers for rendering.
     *
     * All of the layer's children are rendered on top of the shared canvas element. All children that extend
     * {@link MapSharedCachedCanvasSubLayer} are treated as sublayers and can render to the shared canvas element.
     */
    class MapSharedCachedCanvasLayer extends MapLayer {
        constructor() {
            super(...arguments);
            this.canvasLayerRef = exports.FSComponent.createRef();
            this.sublayers = [];
            this.isInit = false;
        }
        /** @inheritDoc */
        onVisibilityChanged(isVisible) {
            if (!this.isInit) {
                return;
            }
            this.canvasLayerRef.instance.setVisible(isVisible);
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].setVisible(isVisible);
            }
        }
        /** @inheritDoc */
        onAfterRender(thisNode) {
            this.thisNode = thisNode;
            // Enumerate sublayers
            exports.FSComponent.visitNodes(thisNode, node => {
                if (node !== thisNode && node.instance instanceof DisplayComponent) {
                    if (node.instance instanceof MapSharedCachedCanvasSubLayer) {
                        this.sublayers.push(node.instance);
                    }
                    return true;
                }
                return false;
            });
        }
        /** @inheritDoc */
        onAttached() {
            super.onAttached();
            this.canvasLayerRef.instance.onAttached();
            this.sharedDisplayCanvasInstance = new MapSharedCachedCanvasInstanceClass(this.canvasLayerRef.instance.display, this.canvasLayerRef.instance);
            if (this.props.useBuffer) {
                this.sharedBufferCanvasInstance = new MapSharedCachedCanvasInstanceClass(this.canvasLayerRef.instance.buffer, this.canvasLayerRef.instance);
            }
            this.isInit = true;
            if (!this.isVisible()) {
                this.onVisibilityChanged(false);
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].attach(this.props.mapProjection, this.sharedDisplayCanvasInstance, this.sharedBufferCanvasInstance);
            }
        }
        /** @inheritDoc */
        onWake() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onWake();
            }
        }
        /** @inheritDoc */
        onSleep() {
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onSleep();
            }
        }
        /** @inheritDoc */
        onMapProjectionChanged(projection, changeFlags) {
            this.canvasLayerRef.instance.onMapProjectionChanged(projection, changeFlags);
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onMapProjectionChanged(projection, changeFlags);
            }
        }
        /** @inheritDoc */
        onUpdated(time, elapsed) {
            this.canvasLayerRef.instance.onUpdated(time, elapsed);
            let invalidateDisplay = this.canvasLayerRef.instance.display.isInvalid;
            let invalidateBuffer = this.sharedBufferCanvasInstance ? this.canvasLayerRef.instance.buffer.isInvalid : true;
            for (let i = 0; (!invalidateDisplay || !invalidateBuffer) && i < this.sublayers.length; i++) {
                invalidateBuffer || (invalidateBuffer = this.sublayers[i].shouldInvalidateBuffer(time, elapsed));
                invalidateDisplay || (invalidateDisplay = this.sublayers[i].shouldInvalidateDisplay(time, elapsed));
            }
            if (this.sharedBufferCanvasInstance && invalidateBuffer) {
                this.sharedBufferCanvasInstance.invalidate();
                this.sharedBufferCanvasInstance.syncWithMapProjection(this.props.mapProjection);
            }
            if (invalidateDisplay) {
                this.sharedDisplayCanvasInstance.invalidate();
                this.sharedDisplayCanvasInstance.syncWithMapProjection(this.props.mapProjection);
            }
            for (let i = 0; i < this.sublayers.length; i++) {
                this.sublayers[i].onUpdated(time, elapsed);
            }
            this.sharedDisplayCanvasInstance.revalidate();
            if (this.sharedBufferCanvasInstance) {
                this.sharedBufferCanvasInstance.revalidate();
            }
        }
        /** @inheritDoc */
        render() {
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null,
                exports.FSComponent.buildComponent(MapCachedCanvasLayer, { ref: this.canvasLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, useBuffer: this.props.useBuffer, overdrawFactor: this.props.overdrawFactor }),
                this.props.children));
        }
        /** @inheritDoc */
        destroy() {
            this.thisNode && exports.FSComponent.shallowDestroy(this.thisNode);
            super.destroy();
        }
    }
    /**
     * A sublayer of {@link MapSharedCachedCanvasLayer}.
     */
    class MapSharedCachedCanvasSubLayer extends DisplayComponent {
        constructor() {
            super(...arguments);
            this._isAttached = false;
            this._isVisible = true;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's map projection.
         * @throws Error if this sublayer is not attached.
         */
        get projection() {
            if (this._projection) {
                return this._projection;
            }
            throw new Error('MapSharedCachedCanvasSubLayer: attempted to access projection before sublayer was attached');
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's shared displayed canvas instance.
         * @throws Error if this sublayer is not attached.
         */
        get display() {
            if (this._display) {
                return this._display;
            }
            throw new Error('MapSharedCachedCanvasSubLayer: attempted to access display canvas before sublayer was attached');
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * This sublayer's shared offscreen buffer canvas instance.
         * @throws Error if the buffer does not exist.
         */
        get buffer() {
            if (this._buffer) {
                return this._buffer;
            }
            throw new Error('MapSharedCachedCanvasSubLayer: attempted to access buffer canvas when one does not exist');
        }
        /**
         * Attempts to get this sublayer's shared offscreen buffer canvas instance.
         * @returns This sublayer's shared offscreen buffer canvas instance, or `undefined` if the buffer does not exist.
         */
        tryGetBuffer() {
            return this._buffer;
        }
        /**
         * Checks whether this sublayer is attached to a parent layer.
         * @returns Whether this sublayer is attached to a parent layer.
         */
        isAttached() {
            return this._isAttached;
        }
        /**
         * Checks whether this sublayer is visible.
         * @returns Whether this sublayer is visible.
         */
        isVisible() {
            return this._isVisible;
        }
        /**
         * Attaches this sublayer to a parent layer.
         * @param projection The map projection used by this sublayer.
         * @param display The displayed canvas instance shared by this sublayer.
         * @param buffer The offscreen buffer canvas instance shared by this sublayer, or `undefined` if there is no buffer.
         */
        attach(projection, display, buffer) {
            this._projection = projection;
            this._display = display;
            this._buffer = buffer;
            this._isAttached = true;
            if (!this._isVisible) {
                this.onVisibilityChanged(this._isVisible);
            }
            this.onAttached();
        }
        /**
         * Sets this sublayer's visibility.
         * @param val Whether this sublayer should be visible.
         */
        setVisible(val) {
            if (this._isVisible === val) {
                return;
            }
            this._isVisible = val;
            if (this._isAttached) {
                this.onVisibilityChanged(val);
            }
        }
        /**
         * This method is called when this layer's visibility changes.
         * @param isVisible Whether the layer is now visible.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onVisibilityChanged(isVisible) {
            // noop
        }
        /**
         * This method is called when this sublayer is attached to its parent layer.
         */
        onAttached() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is awakened.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this sublayer's parent layer is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when this sublayer's map projection changes.
         * @param projection This sublayer's map projection.
         * @param changeFlags The types of changes made to the projection.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onMapProjectionChanged(projection, changeFlags) {
            // noop
        }
        /**
         * This method is called at the beginning of every update cycle to check whether this sublayer's shared displayed
         * canvas instance should be invalidated. If the canvas is already invalidated, then this method will not be called.
         * @param time The current time as a Javascript timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         * @returns Whether this sublayer's shared displayed canvas instance should be invalidated.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        shouldInvalidateDisplay(time, elapsed) {
            return false;
        }
        /**
         * This method is called at the beginning of every update cycle to check whether this sublayer's shared offscreen
         * buffer canvas instance should be invalidated. If the canvas is already invalidated, then this method will not be
         * called.
         * @param time The current time as a Javascript timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         * @returns Whether this sublayer's shared offscreen buffer canvas instance should be invalidated.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        shouldInvalidateBuffer(time, elapsed) {
            return false;
        }
        /**
         * This method is called once every update cycle after this sublayer's shared canvas instance has had a chance to be
         * invalidated.
         * @param time The current time as a Javascript timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            // noop
        }
        /** @inheritdoc */
        render() {
            return null;
        }
    }
    /**
     * An implementation of {@link MapSharedCachedCanvasInstance} which is backed by a
     * {@link MapCachedCanvasLayerCanvasInstance}.
     */
    class MapSharedCachedCanvasInstanceClass {
        /** @inheritDoc */
        get isInvalidated() {
            return this._isInvalidated || this.instance.isInvalid;
        }
        /** @inheritDoc */
        get size() {
            return this.layer.getSize();
        }
        /** @inheritDoc */
        get isTransformInvalidated() {
            return this.instance.isInvalid;
        }
        /** @inheritDoc */
        get reference() {
            return this.instance.reference;
        }
        /** @inheritDoc */
        get transform() {
            return this.instance.transform;
        }
        /** @inheritDoc */
        get geoProjection() {
            return this.instance.geoProjection;
        }
        /**
         * Creates a new instance of MapSharedCachedCanvasInstanceClass.
         * @param instance This instance's backing canvas instance.
         * @param layer The backing canvas instance's parent cached canvas layer.
         */
        constructor(instance, layer) {
            this.instance = instance;
            this.layer = layer;
            /** @inheritDoc */
            this.canvas = this.instance.canvas;
            /** @inheritDoc */
            this.context = this.instance.context;
            this._isInvalidated = false;
        }
        /**
         * Syncs this canvas instance with the current map projection.
         * @param mapProjection The current map projection.
         */
        syncWithMapProjection(mapProjection) {
            this.instance.syncWithMapProjection(mapProjection);
        }
        /**
         * Invalidates and clears this canvas.
         */
        invalidate() {
            this._isInvalidated = true;
            this.instance.invalidate();
        }
        /**
         * Revalidates this canvas.
         */
        revalidate() {
            this._isInvalidated = false;
        }
    }

    /**
     * A map controller.
     */
    class MapSystemController {
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether this controller is alive. */
        get isAlive() {
            return this._isAlive;
        }
        /**
         * Constructor.
         * @param context This controller's map context.
         */
        constructor(context) {
            this._isAlive = true;
            this.context = context;
        }
        /**
         * This method is called after this controller' map is rendered.
         * @param ref A reference to the rendered map.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onAfterMapRender(ref) {
            // noop
        }
        /**
         * This method is called when the dead zone of this controller's map changes.
         * @param deadZone The map's new dead zone.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onDeadZoneChanged(deadZone) {
            // noop
        }
        /**
         * This method is called when the projection of this controller's map changes.
         * @param mapProjection The map projection.
         * @param changeFlags Bit flags describing the type of change.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onMapProjectionChanged(mapProjection, changeFlags) {
            // noop
        }
        /**
         * This method is called immediately before this controller's map updates its layers.
         * @param time The current time, as a Javascript timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onBeforeUpdated(time, elapsed) {
            // noop
        }
        /**
         * This method is called immediately after this controller's map updates its layers.
         * @param time The current time, as a Javascript timestamp.
         * @param elapsed The elapsed time, in milliseconds, since the last update.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onAfterUpdated(time, elapsed) {
            // noop
        }
        /**
         * This method is called when this controller's map is awakened.
         */
        onWake() {
            // noop
        }
        /**
         * This method is called when this controller's map is put to sleep.
         */
        onSleep() {
            // noop
        }
        /**
         * This method is called when this controller's map is destroyed.
         */
        onMapDestroyed() {
            // noop
        }
        /**
         * Destroys this controller.
         */
        destroy() {
            this._isAlive = false;
        }
    }

    /**
     * A controller which maintains an arbitrary number of bindings.
     */
    class MapBindingsController extends MapSystemController {
        /**
         * Constructor.
         * @param context This controller's map context.
         * @param bindings This controller's bindings.
         * @param onDestroy A function to execute when the controller is destroyed.
         */
        constructor(context, bindings, onDestroy) {
            super(context);
            this.bindings = bindings;
            this.onDestroy = onDestroy;
            this.maps = [];
            this.pipes = [];
        }
        /** @inheritdoc */
        onAfterMapRender() {
            for (const binding of this.bindings) {
                if ('map' in binding) {
                    if ('sources' in binding) {
                        const map = MappedSubject.create(...binding.sources);
                        this.maps.push(map);
                        this.pipes.push(map.pipe(binding.target, binding.map));
                    }
                    else {
                        this.pipes.push(binding.source.pipe(binding.target, binding.map));
                    }
                }
                else {
                    this.pipes.push(binding.source.pipe(binding.target));
                }
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        onWake() {
            this.maps.forEach(map => { map.resume(); });
            this.pipes.forEach(pipe => { pipe.resume(true); });
        }
        /** @inheritdoc */
        onSleep() {
            this.maps.forEach(map => { map.pause(); });
            this.pipes.forEach(pipe => { pipe.pause(); });
        }
        /** @inheritdoc */
        destroy() {
            this.onDestroy && this.onDestroy();
            this.maps.forEach(map => { map.destroy(); });
            this.pipes.forEach(pipe => { pipe.destroy(); });
            super.destroy();
        }
    }

    /**
     * Updates a map at regular intervals based on event bus clock events.
     */
    class MapClockUpdateController extends MapSystemController {
        /** @inheritdoc */
        onAfterMapRender(ref) {
            this.freqSub = this.context.updateFreq.sub(freq => {
                var _a;
                (_a = this.clockSub) === null || _a === void 0 ? void 0 : _a.destroy();
                this.clockSub = this.context.bus.getSubscriber().on('realTime').atFrequency(freq).handle(realTime => {
                    ref.update(realTime);
                });
            }, true);
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            super.destroy();
            (_a = this.clockSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.freqSub) === null || _b === void 0 ? void 0 : _b.destroy();
        }
    }

    /**
     * Controls the map system's flight plan module.
     */
    class MapFlightPlanController extends MapSystemController {
        constructor() {
            super(...arguments);
            this.flightPlanModule = this.context.model.getModule(MapSystemKeys.FlightPlan);
            this.planCopiedHandler = (evt) => {
                this.flightPlanModule.getPlanSubjects(evt.targetPlanIndex).flightPlan.set(this.context[MapSystemKeys.FlightPlanner].getFlightPlan(evt.targetPlanIndex));
                this.flightPlanModule.getPlanSubjects(evt.targetPlanIndex).planChanged.notify(this);
            };
            this.planCreatedHandler = (evt) => {
                this.flightPlanModule.getPlanSubjects(evt.planIndex).flightPlan.set(this.context[MapSystemKeys.FlightPlanner].getFlightPlan(evt.planIndex));
            };
            this.planDeletedHandler = (evt) => {
                this.flightPlanModule.getPlanSubjects(evt.planIndex).flightPlan.set(undefined);
            };
            this.planChangeHandler = (evt) => {
                this.flightPlanModule.getPlanSubjects(evt.planIndex).planChanged.notify(this);
            };
            this.planCalculatedHandler = (evt) => {
                this.flightPlanModule.getPlanSubjects(evt.planIndex).planCalculated.notify(this);
            };
            this.activeLegChangedHandler = (evt) => {
                if (evt.type === exports.ActiveLegType.Lateral) {
                    this.flightPlanModule.getPlanSubjects(evt.planIndex).activeLeg.set(evt.index);
                }
            };
        }
        /** @inheritdoc */
        onAfterMapRender() {
            const flightPlanner = this.context[MapSystemKeys.FlightPlanner];
            this.fplCopiedSub = flightPlanner.onEvent('fplCopied').handle(this.planCopiedHandler);
            this.fplCreatedSub = flightPlanner.onEvent('fplCreated').handle(this.planCreatedHandler);
            this.fplDeletedSub = flightPlanner.onEvent('fplDeleted').handle(this.planDeletedHandler);
            this.fplDirectToDataChangedSub = flightPlanner.onEvent('fplDirectToDataChanged').handle(this.planChangeHandler);
            this.fplLoadedSub = flightPlanner.onEvent('fplLoaded').handle(this.planCreatedHandler);
            this.fplOriginDestChangedSub = flightPlanner.onEvent('fplOriginDestChanged').handle(this.planChangeHandler);
            this.fplProcDetailsChangedSub = flightPlanner.onEvent('fplProcDetailsChanged').handle(this.planChangeHandler);
            this.fplSegmentChangeSub = flightPlanner.onEvent('fplSegmentChange').handle(this.planChangeHandler);
            this.fplUserDataDeleteSub = flightPlanner.onEvent('fplUserDataDelete').handle(this.planChangeHandler);
            this.fplUserDataSetSub = flightPlanner.onEvent('fplUserDataSet').handle(this.planChangeHandler);
            this.fplActiveLegChangeSub = flightPlanner.onEvent('fplActiveLegChange').handle(this.activeLegChangedHandler);
            this.fplCalculatedSub = flightPlanner.onEvent('fplCalculated').handle(this.planCalculatedHandler);
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
            super.destroy();
            (_a = this.fplCopiedSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.fplCreatedSub) === null || _b === void 0 ? void 0 : _b.destroy();
            (_c = this.fplDeletedSub) === null || _c === void 0 ? void 0 : _c.destroy();
            (_d = this.fplDirectToDataChangedSub) === null || _d === void 0 ? void 0 : _d.destroy();
            (_e = this.fplLoadedSub) === null || _e === void 0 ? void 0 : _e.destroy();
            (_f = this.fplOriginDestChangedSub) === null || _f === void 0 ? void 0 : _f.destroy();
            (_g = this.fplProcDetailsChangedSub) === null || _g === void 0 ? void 0 : _g.destroy();
            (_h = this.fplSegmentChangeSub) === null || _h === void 0 ? void 0 : _h.destroy();
            (_j = this.fplUserDataDeleteSub) === null || _j === void 0 ? void 0 : _j.destroy();
            (_k = this.fplUserDataSetSub) === null || _k === void 0 ? void 0 : _k.destroy();
            (_l = this.fplActiveLegChangeSub) === null || _l === void 0 ? void 0 : _l.destroy();
            (_m = this.fplCalculatedSub) === null || _m === void 0 ? void 0 : _m.destroy();
        }
    }

    /**
     * Controls the target position of a map to follow the player airplane.
     */
    class MapFollowAirplaneController extends MapSystemController {
        constructor() {
            super(...arguments);
            this.ownAirplanePropsModule = this.context.model.getModule(MapSystemKeys.OwnAirplaneProps);
            this.isFollowingAirplane = this.context.model.getModule(MapSystemKeys.FollowAirplane).isFollowing;
            this.mapProjectionParams = {
                target: new GeoPoint(0, 0)
            };
            this.targetControl = this.context[MapSystemKeys.TargetControl];
            this.targetControlConsumer = {
                priority: 0,
                onAcquired: () => {
                    this.isFollowingAirplane.set(true);
                },
                onCeded: () => {
                    this.isFollowingAirplane.set(false);
                }
            };
        }
        /** @inheritdoc */
        onAfterMapRender() {
            this.targetControl.claim(this.targetControlConsumer);
        }
        /** @inheritdoc */
        onBeforeUpdated() {
            if (this.isFollowingAirplane.get()) {
                this.mapProjectionParams.target.set(this.ownAirplanePropsModule.position.get());
                this.context.projection.setQueued(this.mapProjectionParams);
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            super.destroy();
            this.targetControl.forfeit(this.targetControlConsumer);
        }
    }

    /**
     * An enumeration of possible map rotation types.
     */
    exports.MapRotation = void 0;
    (function (MapRotation) {
        /** Map up position does not follow a defined pattern. */
        MapRotation["Undefined"] = "Undefined";
        /** Map up position points towards true north. */
        MapRotation["NorthUp"] = "NorthUp";
        /** Map up position points towards the current airplane track. */
        MapRotation["TrackUp"] = "TrackUp";
        /** Map up position points towards the current airplane heading. */
        MapRotation["HeadingUp"] = "HeadingUp";
        /** Map up position points towards the current desired track. */
        MapRotation["DtkUp"] = "DtkUp";
    })(exports.MapRotation || (exports.MapRotation = {}));
    /**
     * A module describing the rotation behavior of the map.
     */
    class MapRotationModule {
        constructor() {
            /** The type of map rotation to use. */
            this.rotationType = Subject.create(exports.MapRotation.HeadingUp);
            /** The desired track, in degrees true. */
            this.dtk = Subject.create(0);
        }
    }

    /**
     * Controls the orientation of the own airplane icon set in {@link MapOwnAirplaneIconModule} based on a desired
     * orientation and the map rotation type. If the desired orientation matches the map rotation (e.g. both Heading Up),
     * the icon orientation is set to Map Up; otherwise the orientation is set to the desired orientation.
     */
    class MapOwnAirplaneIconOrientationController extends MapSystemController {
        /**
         * Constructor.
         * @param context This controller's map context.
         * @param desiredOrientation The desired orientation of the own airplane icon.
         */
        constructor(context, desiredOrientation) {
            super(context);
            this.ownAirplaneIconModule = this.context.model.getModule(MapSystemKeys.OwnAirplaneIcon);
            this.needUpdateIconOrientation = false;
            this.desiredIconOrientation = SubscribableUtils.toSubscribable(desiredOrientation, true);
            this.orientationState = MappedSubject.create(this.desiredIconOrientation, this.context.model.getModule(MapSystemKeys.Rotation).rotationType);
        }
        /** @inheritdoc */
        onAfterMapRender() {
            var _a;
            (_a = this.orientationState) === null || _a === void 0 ? void 0 : _a.sub(() => { this.needUpdateIconOrientation = true; }, true);
        }
        /** @inheritdoc */
        onBeforeUpdated() {
            if (this.needUpdateIconOrientation) {
                const [desiredOrientation, rotation] = this.orientationState.get();
                if ((desiredOrientation === exports.MapOwnAirplaneIconOrientation.HeadingUp && rotation === exports.MapRotation.HeadingUp)
                    || (desiredOrientation === exports.MapOwnAirplaneIconOrientation.TrackUp && rotation === exports.MapRotation.TrackUp)) {
                    this.ownAirplaneIconModule.orientation.set(exports.MapOwnAirplaneIconOrientation.MapUp);
                }
                else {
                    this.ownAirplaneIconModule.orientation.set(desiredOrientation);
                }
                this.needUpdateIconOrientation = false;
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            this.orientationState.destroy();
            super.destroy();
        }
    }

    /**
     * Updates the properties in a {@link MapOwnAirplanePropsModule}.
     */
    class MapOwnAirplanePropsController extends MapSystemController {
        /**
         * Constructor.
         * @param context This controller's map context.
         * @param properties The properties to update on the module.
         * @param updateFreq A subscribable which provides the update frequency, in hertz.
         */
        constructor(context, properties, updateFreq) {
            super(context);
            this.properties = properties;
            this.updateFreq = updateFreq;
            this.module = this.context.model.getModule(MapSystemKeys.OwnAirplaneProps);
            this.subs = {};
        }
        /** @inheritdoc */
        onAfterMapRender() {
            const sub = this.context.bus.getSubscriber();
            this.updateFreqSub = this.updateFreq.sub(freq => {
                var _a;
                for (const property of this.properties) {
                    (_a = this.subs[property]) === null || _a === void 0 ? void 0 : _a.destroy();
                    this.subs[property] = this.bindProperty(sub, property, freq);
                }
            }, true);
        }
        /**
         * Binds a module property to data received through the event bus.
         * @param sub The event bus subscriber.
         * @param property The property to bind.
         * @param updateFreq The data update frequency.
         * @returns The subscription created by the binding.
         */
        bindProperty(sub, property, updateFreq) {
            switch (property) {
                case 'position':
                    return sub.on('gps-position').atFrequency(updateFreq).handle(lla => { this.module.position.set(lla.lat, lla.long); });
                case 'altitude':
                    return sub.on('indicated_alt').atFrequency(updateFreq).handle(alt => { this.module.altitude.set(alt, UnitType.FOOT); });
                case 'groundSpeed':
                    return sub.on('ground_speed').atFrequency(updateFreq).handle(gs => { this.module.groundSpeed.set(gs, UnitType.KNOT); });
                case 'hdgTrue':
                    return sub.on('hdg_deg_true').atFrequency(updateFreq).handle(hdg => { this.module.hdgTrue.set(hdg); });
                case 'trackTrue':
                    return sub.on('track_deg_true').atFrequency(updateFreq).handle(track => { this.module.trackTrue.set(track); });
                case 'verticalSpeed':
                    return sub.on('vertical_speed').atFrequency(updateFreq).handle(vs => { this.module.verticalSpeed.set(vs, UnitType.FPM); });
                case 'turnRate':
                    return sub.on('delta_heading_rate').atFrequency(updateFreq).handle(turnRate => { this.module.turnRate.set(turnRate); });
                case 'isOnGround':
                    return sub.on('on_ground').atFrequency(updateFreq).handle(isOnGround => { this.module.isOnGround.set(isOnGround); });
                case 'magVar':
                    return sub.on('magvar').atFrequency(updateFreq).handle(magVar => { this.module.magVar.set(magVar); });
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            super.destroy();
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            for (const property of this.properties) {
                (_b = this.subs[property]) === null || _b === void 0 ? void 0 : _b.destroy();
            }
        }
    }

    /// <reference types="@microsoft/msfs-types/js/avionics" />
    /**
     * Controls the rotation of a map based on the behavior defined in {@link MapRotationModule}.
     */
    class MapRotationController extends MapSystemController {
        constructor() {
            super(...arguments);
            this.rotationModule = this.context.model.getModule(MapSystemKeys.Rotation);
            this.ownAirplanePropsModule = this.context.model.getModule(MapSystemKeys.OwnAirplaneProps);
            this.rotationParam = {
                rotation: 0
            };
            this.hasRotationControl = false;
            this.rotationControl = this.context[MapSystemKeys.RotationControl];
            this.rotationControlConsumer = {
                priority: 0,
                onAcquired: () => {
                    this.hasRotationControl = true;
                },
                onCeded: () => {
                    this.hasRotationControl = false;
                }
            };
            this.rotationFuncs = {
                [exports.MapRotation.NorthUp]: () => 0,
                [exports.MapRotation.HeadingUp]: this.ownAirplanePropsModule === undefined
                    ? () => 0
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    : () => -this.ownAirplanePropsModule.hdgTrue.get() * Avionics.Utils.DEG2RAD,
                [exports.MapRotation.TrackUp]: this.ownAirplanePropsModule === undefined
                    ? () => 0
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    : () => this.ownAirplanePropsModule.groundSpeed.get().asUnit(UnitType.KNOT) < 5
                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                        ? -this.ownAirplanePropsModule.hdgTrue.get() * Avionics.Utils.DEG2RAD
                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                        : -this.ownAirplanePropsModule.trackTrue.get() * Avionics.Utils.DEG2RAD,
                [exports.MapRotation.DtkUp]: () => -this.rotationModule.dtk.get() * Avionics.Utils.DEG2RAD
            };
        }
        /** @inheritdoc */
        onAfterMapRender() {
            this.rotationSub = this.rotationModule.rotationType.sub(type => {
                this.rotationFunc = this.rotationFuncs[type];
            }, true);
            this.rotationControl.claim(this.rotationControlConsumer);
        }
        /** @inheritdoc */
        onBeforeUpdated() {
            if (this.hasRotationControl && this.rotationFunc !== undefined) {
                this.rotationParam.rotation = this.rotationFunc();
                this.context.projection.setQueued(this.rotationParam);
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            super.onMapDestroyed();
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            super.destroy();
            (_a = this.rotationSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.rotationControl.forfeit(this.rotationControlConsumer);
        }
    }

    /**
     * Waypoint roles used by the map system waypoint display system.
     */
    exports.MapSystemWaypointRoles = void 0;
    (function (MapSystemWaypointRoles) {
        /** The normal waypoint display role. */
        MapSystemWaypointRoles["Normal"] = "Normal";
        /** The waypoint role for displaying waypoints along the flight plan. */
        MapSystemWaypointRoles["FlightPlan"] = "FlightPlan";
    })(exports.MapSystemWaypointRoles || (exports.MapSystemWaypointRoles = {}));

    /**
     * A map system layer that draws the flight plan.
     */
    class MapSystemFlightPlanLayer extends MapLayer {
        constructor() {
            var _a;
            super(...arguments);
            this.instanceId = MapSystemFlightPlanLayer.instanceId++;
            this.flightPathLayerRef = exports.FSComponent.createRef();
            this.waypointLayerRef = exports.FSComponent.createRef();
            this.defaultRoleId = (_a = this.props.waypointRenderer.getRoleFromName(exports.MapSystemWaypointRoles.FlightPlan)) !== null && _a !== void 0 ? _a : 0;
            this.planModule = this.props.model.getModule(MapSystemKeys.FlightPlan);
            this.waypointPrefix = `${MapSystemFlightPlanLayer.WAYPOINT_PREFIX}_${this.instanceId}`;
            this.legWaypoints = new Map();
            this.waypointsUpdating = false;
            this.waypointId = 0;
            this.facLoader = new FacilityLoader(FacilityRepository.getRepository(this.props.bus));
            this.facWaypointCache = DefaultFacilityWaypointCache.getCache(this.props.bus);
            this.clipBounds = VecNSubject.create(new Float64Array(4));
            this.clippedPathStream = new ClippedPathStream(NullPathStream.INSTANCE, this.clipBounds);
            this.pathStreamStack = new GeoProjectionPathStreamStack(NullPathStream.INSTANCE, this.props.mapProjection.getGeoProjection(), Math.PI / 12, 0.25, 8);
            this.updateScheduled = false;
        }
        /** @inheritdoc */
        onAttached() {
            this.flightPathLayerRef.instance.onAttached();
            this.waypointLayerRef.instance.onAttached();
            this.pathStreamStack.pushPostProjected(this.clippedPathStream);
            this.pathStreamStack.setConsumer(this.flightPathLayerRef.instance.display.context);
            this.initWaypointRenderer();
            this.planModule.getPlanSubjects(this.props.planIndex).flightPlan.sub(() => this.updateScheduled = true);
            this.planModule.getPlanSubjects(this.props.planIndex).planCalculated.on(() => this.updateScheduled = true);
            this.planModule.getPlanSubjects(this.props.planIndex).planChanged.on(() => this.updateScheduled = true);
            this.planModule.getPlanSubjects(this.props.planIndex).activeLeg.sub(() => this.updateScheduled = true);
            this.props.waypointRenderer.onRolesAdded.on(() => this.initWaypointRenderer());
            super.onAttached();
        }
        /**
         * Initializes the waypoint renderer for this layer.
         */
        initWaypointRenderer() {
            let hasDefaultRole = false;
            const flightPlanRoles = this.props.waypointRenderer.getRoleNamesByGroup(`${exports.MapSystemWaypointRoles.FlightPlan}_${this.props.planIndex}`);
            for (let i = 0; i < flightPlanRoles.length; i++) {
                const roleId = this.props.waypointRenderer.getRoleFromName(flightPlanRoles[i]);
                if (roleId !== undefined) {
                    this.props.waypointRenderer.setCanvasContext(roleId, this.waypointLayerRef.instance.display.context);
                    this.props.waypointRenderer.setIconFactory(roleId, this.props.iconFactory);
                    this.props.waypointRenderer.setLabelFactory(roleId, this.props.labelFactory);
                    if (!hasDefaultRole) {
                        this.props.flightPathRenderer.defaultRoleId = roleId;
                        hasDefaultRole = true;
                    }
                }
            }
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            this.flightPathLayerRef.instance.onUpdated(time, elapsed);
            this.waypointLayerRef.instance.onUpdated(time, elapsed);
            if (this.isVisible()) {
                const display = this.flightPathLayerRef.instance.display;
                if (display.isInvalid) {
                    display.clear();
                    display.syncWithMapProjection(this.props.mapProjection);
                    this.updateScheduled = true;
                }
                if (this.updateScheduled) {
                    if (!this.waypointsUpdating) {
                        this.updateWaypoints();
                    }
                    const context = display.context;
                    display.clear();
                    const plan = this.planModule.getPlanSubjects(this.props.planIndex).flightPlan.get();
                    if (plan !== undefined) {
                        this.pathStreamStack.setProjection(display.geoProjection);
                        this.props.flightPathRenderer.render(plan, undefined, undefined, context, this.pathStreamStack);
                    }
                    this.updateScheduled = false;
                }
            }
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.flightPathLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            this.waypointLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            const size = this.flightPathLayerRef.instance.getSize();
            this.clipBounds.set(-MapSystemFlightPlanLayer.CLIP_BOUNDS_BUFFER, -MapSystemFlightPlanLayer.CLIP_BOUNDS_BUFFER, size + MapSystemFlightPlanLayer.CLIP_BOUNDS_BUFFER, size + MapSystemFlightPlanLayer.CLIP_BOUNDS_BUFFER);
        }
        /** @inheritdoc */
        setVisible(val) {
            super.setVisible(val);
            this.waypointLayerRef.instance.setVisible(val);
            this.flightPathLayerRef.instance.setVisible(val);
        }
        /**
         * Updates waypoints for the flight plan.
         * @throws An error if the waypoints are already updating.
         */
        async updateWaypoints() {
            if (this.waypointsUpdating) {
                throw new Error('A flight plan waypoint update is already in progress.');
            }
            this.waypointsUpdating = true;
            const flightPlan = this.planModule.getPlanSubjects(this.props.planIndex).flightPlan.get();
            const activeLegIndex = this.planModule.getPlanSubjects(this.props.planIndex).activeLeg.get();
            if (flightPlan === undefined) {
                for (const legWaypoint of this.legWaypoints.values()) {
                    const [waypoint, roleId] = legWaypoint;
                    this.props.waypointRenderer.deregister(waypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                }
                this.legWaypoints.clear();
                this.waypointsUpdating = false;
                return;
            }
            const activeLeg = flightPlan.tryGetLeg(activeLegIndex);
            const legsToDisplay = new Map();
            let legIndex = 0;
            for (const leg of flightPlan.legs()) {
                let roleId = this.defaultRoleId;
                const handler = this.props.flightPathRenderer.legWaypointHandlers.get(this.props.planIndex);
                if (handler !== undefined) {
                    roleId = handler(flightPlan, leg, activeLeg, legIndex, activeLegIndex);
                }
                if (roleId !== 0) {
                    legsToDisplay.set(leg, roleId);
                }
                legIndex++;
            }
            // Remove records of legs that are no longer in the set of legs to display.
            for (const leg of this.legWaypoints) {
                const [legDefinition, legWaypoint] = leg;
                const [waypoint, roleId] = legWaypoint;
                if (!legsToDisplay.has(legDefinition)) {
                    this.props.waypointRenderer.deregister(waypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                    this.legWaypoints.delete(legDefinition);
                }
            }
            const waypointRefreshes = [];
            // Create or refresh waypoints to display
            for (const leg of legsToDisplay) {
                waypointRefreshes.push(this.buildPlanWaypoint(leg[0], leg[1]));
            }
            await Promise.all(waypointRefreshes);
            this.waypointsUpdating = false;
        }
        /**
         * Builds or refreshes a flight plan waypoint.
         * @param leg The leg to build the waypoint for.
         * @param roleId The role ID to assign to the waypoint.
         */
        async buildPlanWaypoint(leg, roleId) {
            switch (leg.leg.type) {
                case exports.LegType.CD:
                case exports.LegType.VD:
                case exports.LegType.CR:
                case exports.LegType.VR:
                case exports.LegType.FC:
                case exports.LegType.FD:
                case exports.LegType.FA:
                case exports.LegType.CA:
                case exports.LegType.VA:
                case exports.LegType.FM:
                case exports.LegType.VM:
                case exports.LegType.CI:
                case exports.LegType.VI:
                    await this.buildTerminatorWaypoint(leg, roleId);
                    break;
                case exports.LegType.Discontinuity:
                case exports.LegType.ThruDiscontinuity:
                    break;
                default:
                    await this.buildFixWaypoint(leg, roleId);
                    break;
            }
        }
        /**
         * Builds a flight path terminator based waypoint.
         * @param leg The leg to build the waypoint for.
         * @param roleId The role ID to assign to the waypoint.
         */
        async buildTerminatorWaypoint(leg, roleId) {
            var _a, _b, _c, _d, _e, _f;
            const currentLeg = this.legWaypoints.get(leg);
            if (currentLeg !== undefined) {
                const [waypoint, currentRoleId] = currentLeg;
                const lastVector = (_a = leg.calculated) === null || _a === void 0 ? void 0 : _a.flightPath[((_b = leg.calculated) === null || _b === void 0 ? void 0 : _b.flightPath.length) - 1];
                if (lastVector !== undefined) {
                    if (!waypoint.location.get().equals(lastVector.endLat, lastVector.endLon)) {
                        this.props.waypointRenderer.deregister(waypoint, currentRoleId, exports.MapSystemWaypointRoles.FlightPlan);
                        const ident = (_c = leg.name) !== null && _c !== void 0 ? _c : '';
                        const newWaypoint = new FlightPathWaypoint(lastVector.endLat, lastVector.endLon, leg, `${this.waypointPrefix}_${this.waypointId++}_${ident}`, ident);
                        this.legWaypoints.set(leg, [newWaypoint, roleId]);
                        this.props.waypointRenderer.register(newWaypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                    }
                    else if (currentRoleId !== roleId) {
                        this.props.waypointRenderer.deregister(waypoint, currentRoleId, exports.MapSystemWaypointRoles.FlightPlan);
                        this.props.waypointRenderer.register(waypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                        this.legWaypoints.set(leg, [waypoint, roleId]);
                    }
                }
                else {
                    this.props.waypointRenderer.deregister(waypoint, currentRoleId, exports.MapSystemWaypointRoles.FlightPlan);
                }
            }
            else {
                const lastVector = (_d = leg.calculated) === null || _d === void 0 ? void 0 : _d.flightPath[((_e = leg.calculated) === null || _e === void 0 ? void 0 : _e.flightPath.length) - 1];
                if (lastVector !== undefined) {
                    const ident = (_f = leg.name) !== null && _f !== void 0 ? _f : '';
                    const newWaypoint = new FlightPathWaypoint(lastVector.endLat, lastVector.endLon, leg, `${this.waypointPrefix}_${this.waypointId++}_${ident}`, ident);
                    this.legWaypoints.set(leg, [newWaypoint, roleId]);
                    this.props.waypointRenderer.register(newWaypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                }
            }
        }
        /**
         * Builds a standard facility fix waypoint for flight plan waypoint display.
         * @param leg The leg to build the waypoint for.
         * @param roleId The role ID to assign to the waypoint.
         */
        async buildFixWaypoint(leg, roleId) {
            var _a;
            const legWaypoint = this.legWaypoints.get(leg);
            if (legWaypoint === undefined) {
                const facIcao = leg.leg.fixIcao;
                let facility;
                try {
                    facility = await this.facLoader.getFacility(ICAO.getFacilityType(facIcao), facIcao);
                }
                catch (err) {
                    /* continue */
                }
                if (facility !== undefined) {
                    const waypoint = this.facWaypointCache.get(facility);
                    const ident = (_a = leg.name) !== null && _a !== void 0 ? _a : '';
                    const newWaypoint = new FlightPathWaypoint(waypoint.location, leg, `${this.waypointPrefix}_${this.waypointId++}_${ident}`, ident);
                    this.props.waypointRenderer.register(newWaypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                    this.legWaypoints.set(leg, [newWaypoint, roleId]);
                }
            }
            else {
                const [waypoint, currentRoleId] = legWaypoint;
                if (currentRoleId !== roleId) {
                    this.props.waypointRenderer.deregister(waypoint, currentRoleId, exports.MapSystemWaypointRoles.FlightPlan);
                    this.props.waypointRenderer.register(waypoint, roleId, exports.MapSystemWaypointRoles.FlightPlan);
                    this.legWaypoints.set(leg, [waypoint, roleId]);
                }
            }
        }
        /** @inheritdoc */
        render() {
            var _a, _b;
            return (exports.FSComponent.buildComponent(exports.FSComponent.Fragment, null,
                exports.FSComponent.buildComponent(MapCachedCanvasLayer, { ref: this.flightPathLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, overdrawFactor: Math.SQRT2, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }),
                exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.waypointLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, class: (_b = this.props.class) !== null && _b !== void 0 ? _b : '' })));
        }
    }
    MapSystemFlightPlanLayer.WAYPOINT_PREFIX = 'MapSystemFplLayer';
    MapSystemFlightPlanLayer.CLIP_BOUNDS_BUFFER = 10;
    MapSystemFlightPlanLayer.instanceId = 0;

    /**
     * ADS-B operating modes.
     */
    exports.AdsbOperatingMode = void 0;
    (function (AdsbOperatingMode) {
        AdsbOperatingMode["Standby"] = "Standby";
        AdsbOperatingMode["Surface"] = "Surface";
        AdsbOperatingMode["Airborne"] = "Airborne";
    })(exports.AdsbOperatingMode || (exports.AdsbOperatingMode = {}));
    /**
     * An ADS-B system.
     */
    class Adsb {
        /**
         * Constructor.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.operatingMode = Subject.create(exports.AdsbOperatingMode.Standby);
            this.eventSubscriber = this.bus.getSubscriber();
        }
        /**
         * Gets this system's operating mode.
         * @returns This system's operating mode.
         */
        getOperatingMode() {
            return this.operatingMode.get();
        }
        /**
         * Sets this system's operating mode.
         * @param mode The new operating mode.
         */
        setOperatingMode(mode) {
            this.operatingMode.set(mode);
        }
        /**
         * Gets an event bus subscriber for TCAS events.
         * @returns an event bus subscriber for TCAS events..
         */
        getEventSubscriber() {
            return this.eventSubscriber;
        }
        /**
         * Initializes this ADS-B system.
         */
        init() {
            this.operatingMode.sub(mode => {
                this.bus.pub('adsb_operating_mode', mode, false, true);
            }, true);
        }
    }

    /**
     * TCAS operating modes.
     */
    exports.TcasOperatingMode = void 0;
    (function (TcasOperatingMode) {
        TcasOperatingMode["Off"] = "Off";
        TcasOperatingMode["Standby"] = "Standby";
        TcasOperatingMode["TAOnly"] = "TAOnly";
        TcasOperatingMode["TA_RA"] = "TA/RA";
        TcasOperatingMode["Test"] = "Test";
        TcasOperatingMode["Failed"] = "Failed";
    })(exports.TcasOperatingMode || (exports.TcasOperatingMode = {}));
    /**
     * TCAS alert level.
     */
    exports.TcasAlertLevel = void 0;
    (function (TcasAlertLevel) {
        TcasAlertLevel[TcasAlertLevel["None"] = 0] = "None";
        TcasAlertLevel[TcasAlertLevel["ProximityAdvisory"] = 1] = "ProximityAdvisory";
        TcasAlertLevel[TcasAlertLevel["TrafficAdvisory"] = 2] = "TrafficAdvisory";
        TcasAlertLevel[TcasAlertLevel["ResolutionAdvisory"] = 3] = "ResolutionAdvisory";
    })(exports.TcasAlertLevel || (exports.TcasAlertLevel = {}));
    /**
     * Bit flags describing TCAS resolution advisories.
     */
    exports.TcasResolutionAdvisoryFlags = void 0;
    (function (TcasResolutionAdvisoryFlags) {
        /** A corrective resolution advisory. Requires a change in the own airplane's vertical speed. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Corrective"] = 1] = "Corrective";
        /** An upward sense resolution advisory. Commands a vertical speed above a certain value. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["UpSense"] = 2] = "UpSense";
        /** A downward sense resolution advisory. Commands a vertical speed below a certain value. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["DownSense"] = 4] = "DownSense";
        /** A resolution advisory which crosses an intruder's altitude. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Crossing"] = 8] = "Crossing";
        /** A CLIMB resolution advisory. Commands a positive vertical speed above 1500 FPM. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Climb"] = 16] = "Climb";
        /** A DESCEND resolution advisory. Commands a negative vertical speed below -1500 FPM. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Descend"] = 32] = "Descend";
        /** An INCREASE CLIMB or INCREASE DESCENT resolution advisory. Commands a vertical speed above 2500 FPM or below -2500 FPM. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Increase"] = 64] = "Increase";
        /** A CLIMB or DESCEND resolution advisory that reverses sense. Commands a vertical speed above 1500 FPM or below -1500 FPM. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["Reversal"] = 128] = "Reversal";
        /** A corrective REDUCE CLIMB resolution advisory. Commands a vertical speed of 0 FPM or less. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["ReduceClimb"] = 256] = "ReduceClimb";
        /** A corrective REDUCE DESCENT resolution advisory. Commands a vertical speed of 0 FPM or more. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["ReduceDescent"] = 512] = "ReduceDescent";
        /** A preventative DO NOT CLIMB resolution advisory. Commands a non-positive vertical speed. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["DoNotClimb"] = 1024] = "DoNotClimb";
        /** A preventative DO NOT DESCEND resolution advisory. Commands a non-negative vertical speed. */
        TcasResolutionAdvisoryFlags[TcasResolutionAdvisoryFlags["DoNotDescend"] = 2048] = "DoNotDescend";
    })(exports.TcasResolutionAdvisoryFlags || (exports.TcasResolutionAdvisoryFlags = {}));
    /**
     * Types of TCAS resolution advisories.
     */
    exports.TcasResolutionAdvisoryType = void 0;
    (function (TcasResolutionAdvisoryType) {
        /** Upward sense, positive, corrective, required vertical speed 1500 to 2000 fpm. */
        TcasResolutionAdvisoryType["Climb"] = "Climb";
        /** Upward sense, positive, corrective, crosses intruder altitude, required vertical speed 1500 to 2000 fpm. */
        TcasResolutionAdvisoryType["CrossingClimb"] = "CrossingClimb";
        /** Upward sense, positive, corrective, required vertical speed 1500 to 4400 fpm. */
        TcasResolutionAdvisoryType["MaintainClimb"] = "MaintainClimb";
        /** Upward sense, positive, corrective, crosses intruder altitude, required vertical speed 1500 to 2000 fpm. */
        TcasResolutionAdvisoryType["CrossingMaintainClimb"] = "CrossingMaintainClimb";
        /** Upward sense, positive, corrective, required vertical speed 2500 to 3000 fpm. */
        TcasResolutionAdvisoryType["IncreaseClimb"] = "IncreaseClimb";
        /** Upward sense, positive, corrective, transition from downward sense, required vertical speed 1500 to 2000 fpm. */
        TcasResolutionAdvisoryType["ReversalClimb"] = "ReversalClimb";
        /** Upward sense, negative, corrective, required vertical speed >= 0 fpm. */
        TcasResolutionAdvisoryType["ReduceDescent"] = "ReduceDescent";
        /** Upward sense, negative, preventative, required vertical speed >= 0 fpm. */
        TcasResolutionAdvisoryType["DoNotDescend0"] = "DoNotDescend0";
        /** Upward sense, negative, preventative, required vertical speed >= -500 fpm. */
        TcasResolutionAdvisoryType["DoNotDescend500"] = "DoNotDescend500";
        /** Upward sense, negative, preventative, required vertical speed >= -1000 fpm. */
        TcasResolutionAdvisoryType["DoNotDescend1000"] = "DoNotDescend1000";
        /** Upward sense, negative, preventative, required vertical speed >= -1500 fpm. */
        TcasResolutionAdvisoryType["DoNotDescend1500"] = "DoNotDescend1500";
        /** Upward sense, negative, preventative, required vertical speed >= -2000 fpm. */
        TcasResolutionAdvisoryType["DoNotDescend2000"] = "DoNotDescend2000";
        /** Downward sense, positive, corrective, required vertical speed -1500 to -2000 fpm. */
        TcasResolutionAdvisoryType["Descend"] = "Descend";
        /** Downward sense, positive, corrective, crosses intruder altitude, required vertical speed -1500 to -2000 fpm. */
        TcasResolutionAdvisoryType["CrossingDescend"] = "CrossingDescend";
        /** Downward sense, positive, corrective, required vertical speed -1500 to -4400 fpm. */
        TcasResolutionAdvisoryType["MaintainDescend"] = "MaintainDescend";
        /** Downward sense, positive, corrective, crosses intruder altitude, required vertical speed -1500 to -4400 fpm. */
        TcasResolutionAdvisoryType["CrossingMaintainDescend"] = "CrossingMaintainDescend";
        /** Downward sense, positive, corrective, required vertical speed -2500 to -3000 fpm. */
        TcasResolutionAdvisoryType["IncreaseDescend"] = "IncreaseDescend";
        /** Downward sense, positive, corrective, transition from upward sense, required vertical speed -1500 to -2000 fpm. */
        TcasResolutionAdvisoryType["ReversalDescend"] = "ReversalDescend";
        /** Downward sense, negative, corrective, required vertical speed <= 0 fpm. */
        TcasResolutionAdvisoryType["ReduceClimb"] = "ReduceClimb";
        /** Downward sense, negative, preventative, required vertical speed <= 0 fpm. */
        TcasResolutionAdvisoryType["DoNotClimb0"] = "DoNotClimb0";
        /** Downward sense, negative, preventative, required vertical speed <= 500 fpm. */
        TcasResolutionAdvisoryType["DoNotClimb500"] = "DoNotClimb500";
        /** Downward sense, negative, preventative, required vertical speed <= 1000 fpm. */
        TcasResolutionAdvisoryType["DoNotClimb1000"] = "DoNotClimb1000";
        /** Downward sense, negative, preventative, required vertical speed <= 1500 fpm. */
        TcasResolutionAdvisoryType["DoNotClimb1500"] = "DoNotClimb1500";
        /** Downward sense, negative, preventative, required vertical speed <= 2000 fpm. */
        TcasResolutionAdvisoryType["DoNotClimb2000"] = "DoNotClimb2000";
        /** Clear of conflict. */
        TcasResolutionAdvisoryType["Clear"] = "Clear";
    })(exports.TcasResolutionAdvisoryType || (exports.TcasResolutionAdvisoryType = {}));
    /**
     * A TCAS-II-like system.
     */
    class Tcas {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param tfcInstrument The traffic instrument which provides traffic contacts for this TCAS.
         * @param maxIntruderCount The maximum number of intruders tracked at any one time by this TCAS.
         * @param realTimeUpdateFreq The maximum update frequency (Hz) in real time.
         * @param simTimeUpdateFreq The maximum update frequency (Hz) in sim time.
         * @param raOptions Options to adjust how resolution advisories are calculated.
         */
        constructor(bus, tfcInstrument, maxIntruderCount, realTimeUpdateFreq, simTimeUpdateFreq, raOptions) {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            this.bus = bus;
            this.tfcInstrument = tfcInstrument;
            this.operatingModeSub = Subject.create(exports.TcasOperatingMode.Standby);
            this.intrudersSorted = [];
            this.intrudersFiltered = [];
            this.intrudersRA = new Set();
            this.contactCreatedHandler = this.onContactAdded.bind(this);
            this.contactRemovedHandler = this.onContactRemoved.bind(this);
            this.ownAirplaneSubs = {
                position: GeoPointSubject.create(new GeoPoint(0, 0)),
                altitude: NumberUnitSubject.create(UnitType.FOOT.createNumber(0)),
                groundTrack: ConsumerSubject.create(null, 0),
                groundSpeed: NumberUnitSubject.create(UnitType.KNOT.createNumber(0)),
                verticalSpeed: NumberUnitSubject.create(UnitType.FPM.createNumber(0)),
                radarAltitude: NumberUnitSubject.create(UnitType.FOOT.createNumber(0)),
                isOnGround: ConsumerSubject.create(null, false)
            };
            this.simTime = ConsumerSubject.create(null, 0);
            this.lastUpdateSimTime = 0;
            this.lastUpdateRealTime = 0;
            this.alertLevelSubs = new Map();
            this.eventPublisher = this.bus.getPublisher();
            this.eventSubscriber = this.bus.getSubscriber();
            this.paSeparationCache = {
                horizontal: UnitType.NMILE.createNumber(0),
                vertical: UnitType.FOOT.createNumber(0)
            };
            this.maxIntruderCount = SubscribableUtils.toSubscribable(maxIntruderCount, true);
            this.realTimeUpdateFreq = SubscribableUtils.toSubscribable(realTimeUpdateFreq, true);
            this.simTimeUpdateFreq = SubscribableUtils.toSubscribable(simTimeUpdateFreq, true);
            this.sensitivity = this.createSensitivity();
            this.ownAirplane = new OwnAirplane(this.ownAirplaneSubs);
            const fullRAOptions = {
                initialResponseTime: ((_a = raOptions === null || raOptions === void 0 ? void 0 : raOptions.initialResponseTime) !== null && _a !== void 0 ? _a : Tcas.DEFAULT_RA_OPTIONS.initialResponseTime).copy(),
                initialAcceleration: ((_b = raOptions === null || raOptions === void 0 ? void 0 : raOptions.initialAcceleration) !== null && _b !== void 0 ? _b : Tcas.DEFAULT_RA_OPTIONS.initialAcceleration).copy(),
                subsequentResponseTime: ((_c = raOptions === null || raOptions === void 0 ? void 0 : raOptions.subsequentResponseTime) !== null && _c !== void 0 ? _c : Tcas.DEFAULT_RA_OPTIONS.subsequentResponseTime).copy(),
                subsequentAcceleration: ((_d = raOptions === null || raOptions === void 0 ? void 0 : raOptions.subsequentAcceleration) !== null && _d !== void 0 ? _d : Tcas.DEFAULT_RA_OPTIONS.subsequentAcceleration).copy(),
                allowClimb: (_e = raOptions === null || raOptions === void 0 ? void 0 : raOptions.allowClimb) !== null && _e !== void 0 ? _e : (() => true),
                allowIncreaseClimb: (_f = raOptions === null || raOptions === void 0 ? void 0 : raOptions.allowIncreaseClimb) !== null && _f !== void 0 ? _f : (() => true),
                allowDescend: (_g = raOptions === null || raOptions === void 0 ? void 0 : raOptions.allowDescend) !== null && _g !== void 0 ? _g : (() => this.ownAirplaneSubs.radarAltitude.get().asUnit(UnitType.FOOT) >= 1100),
                allowIncreaseDescent: (_h = raOptions === null || raOptions === void 0 ? void 0 : raOptions.allowIncreaseDescent) !== null && _h !== void 0 ? _h : (() => this.ownAirplaneSubs.radarAltitude.get().asUnit(UnitType.FOOT) >= 1450)
            };
            this.resolutionAdvisoryHost = new TcasResolutionAdvisoryHostClass(bus, fullRAOptions, this.ownAirplane);
        }
        /**
         * Gets this system's operating mode.
         * @returns This system's operating mode.
         */
        getOperatingMode() {
            return this.operatingModeSub.get();
        }
        /**
         * Sets this system's operating mode.
         * @param mode The new operating mode.
         */
        setOperatingMode(mode) {
            this.operatingModeSub.set(mode);
        }
        /**
         * Gets an array of all currently tracked intruders. The intruders are sorted in order of decreasing threat.
         * @returns an array of all currently tracked intruders.
         */
        getIntruders() {
            return this.intrudersFiltered;
        }
        /**
         * Gets this system's resolution advisory host.
         * @returns This system's resolution advisory host.
         */
        getResolutionAdvisoryHost() {
            return this.resolutionAdvisoryHost;
        }
        /**
         * Gets an event bus subscriber for TCAS events.
         * @returns an event bus subscriber for TCAS events..
         */
        getEventSubscriber() {
            return this.eventSubscriber;
        }
        /**
         * Initializes this system.
         */
        init() {
            // init contact listeners
            const sub = this.bus.getSubscriber();
            this.contactCreatedConsumer = sub.on('traffic_contact_added');
            this.contactRemovedConsumer = sub.on('traffic_contact_removed');
            this.contactCreatedConsumer.handle(this.contactCreatedHandler);
            this.contactRemovedConsumer.handle(this.contactRemovedHandler);
            // add all existing contacts
            this.tfcInstrument.forEachContact(contact => { this.onContactAdded(contact.uid); });
            // init own airplane subjects
            const atFreqSubs = [];
            this.realTimeUpdateFreq.sub(freq => {
                for (const atFreqSub of atFreqSubs) {
                    atFreqSub.destroy();
                }
                atFreqSubs.length = 0;
                atFreqSubs.push(sub.on('gps-position').atFrequency(freq).handle(lla => {
                    this.ownAirplaneSubs.position.set(lla.lat, lla.long);
                    this.ownAirplaneSubs.altitude.set(lla.alt, UnitType.METER);
                }), sub.on('ground_speed').atFrequency(freq).handle(gs => { this.ownAirplaneSubs.groundSpeed.set(gs); }), sub.on('vertical_speed').atFrequency(freq).handle(vs => { this.ownAirplaneSubs.verticalSpeed.set(vs); }), sub.on('radio_alt').atFrequency(freq).handle(alt => { this.ownAirplaneSubs.radarAltitude.set(alt); }));
            }, true);
            this.ownAirplaneSubs.groundTrack.setConsumer(sub.on('track_deg_true'));
            this.ownAirplaneSubs.isOnGround.setConsumer(sub.on('on_ground'));
            // init sim time subject
            this.simTime.setConsumer(sub.on('simTime'));
            // init operating mode notifier
            this.operatingModeSub.sub(this.onOperatingModeChanged.bind(this), true);
            // init update loop
            sub.on('simTime').whenChanged().handle(this.onSimTimeChanged.bind(this));
        }
        /**
         * Responds to changes in this TCAS's operating mode.
         * @param mode The current operating mode.
         */
        onOperatingModeChanged(mode) {
            this.bus.pub('tcas_operating_mode', mode, false, true);
            if (mode !== exports.TcasOperatingMode.TAOnly && mode !== exports.TcasOperatingMode.TA_RA) {
                // We are in a mode where TCAS is not operating normally.
                // Cancel any active resolution advisories.
                this.resolutionAdvisoryHost.cancel(this.simTime.get());
                // Clean up all intruders.
                for (let i = 0; i < this.intrudersFiltered.length; i++) {
                    this.cleanUpIntruder(this.intrudersFiltered[i]);
                }
                this.intrudersFiltered = [];
            }
        }
        /**
         * Sorts two intruders.
         * @param a The first intruder.
         * @param b The second intruder.
         * @returns A negative number if `a` is to be sorted before `b`, a positive number if `b` is to be sorted before `a`,
         * and zero if the two are equal.
         */
        intruderComparator(a, b) {
            // always sort intruders with valid predictions first
            if (a.isPredictionValid && !b.isPredictionValid) {
                return -1;
            }
            else if (!a.isPredictionValid && b.isPredictionValid) {
                return 1;
            }
            else if (a.isPredictionValid) {
                let tcaPredictionA, tcaPredictionB;
                // Always sort intruders predicted to violate RA protected volume first (or for whom an RA has been issued), then
                // TA protected volume (or for whom a TA has been issued)
                const aRA = a.alertLevel.get() === exports.TcasAlertLevel.ResolutionAdvisory || (a.tcaRA.isValid && a.tcaRA.isThreat);
                const bRA = b.alertLevel.get() === exports.TcasAlertLevel.ResolutionAdvisory || (b.tcaRA.isValid && b.tcaRA.isThreat);
                if (aRA && !bRA) {
                    return -1;
                }
                else if (!aRA && bRA) {
                    return 1;
                }
                else if (aRA && bRA) {
                    tcaPredictionA = a.tcaRA;
                    tcaPredictionB = b.tcaRA;
                }
                if (!tcaPredictionA || !tcaPredictionB) {
                    const aTA = a.alertLevel.get() === exports.TcasAlertLevel.TrafficAdvisory || (a.tcaTA.isValid && a.tcaTA.isThreat);
                    const bTA = b.alertLevel.get() === exports.TcasAlertLevel.TrafficAdvisory || (b.tcaTA.isValid && b.tcaTA.isThreat);
                    if (aTA && !bTA) {
                        return -1;
                    }
                    else if (!aTA && bTA) {
                        return 1;
                    }
                    else if (aTA && bTA) {
                        tcaPredictionA = a.tcaTA;
                        tcaPredictionB = b.tcaTA;
                    }
                }
                if (!tcaPredictionA || !tcaPredictionB) {
                    if ((a.tcaRA.isValid || a.tcaTA.isValid) && !b.tcaRA.isValid && !b.tcaTA.isValid) {
                        return -1;
                    }
                    else if ((b.tcaRA.isValid || b.tcaTA.isValid) && !a.tcaRA.isValid && !a.tcaTA.isValid) {
                        return 1;
                    }
                    else {
                        return 0;
                    }
                }
                // If both are predicted to violate the RA or TA protected volume, sort by time to CPA.
                // Otherwise sort by how close they approach the protected volume at CPA.
                const tcaComparison = tcaPredictionA.tcpa.compare(tcaPredictionB.tcpa);
                const normComparison = tcaPredictionA.cpaNorm - tcaPredictionB.cpaNorm;
                let firstComparison;
                let secondComparison;
                if (tcaPredictionA.isThreat) {
                    firstComparison = tcaComparison;
                    secondComparison = normComparison;
                }
                else {
                    firstComparison = normComparison;
                    secondComparison = tcaComparison;
                }
                if (firstComparison === 0) {
                    return secondComparison;
                }
                else {
                    return firstComparison;
                }
            }
            else {
                return 0;
            }
        }
        /**
         * A callback which is called when a new traffic contact is added by this system's traffic instrument.
         * @param uid The ID number of the new contact.
         */
        onContactAdded(uid) {
            const contact = this.tfcInstrument.getContact(uid);
            const intruder = this.createIntruderEntry(contact);
            this.intrudersSorted.push(intruder);
        }
        /**
         * A callback which is called when a traffic contact is removed by this system's traffic instrument.
         * @param uid The ID number of the removed contact.
         */
        onContactRemoved(uid) {
            const sortedIndex = this.intrudersSorted.findIndex(intruder => intruder.contact.uid === uid);
            const culledIndex = this.intrudersFiltered.findIndex(intruder => intruder.contact.uid === uid);
            if (sortedIndex >= 0) {
                this.intrudersSorted.splice(sortedIndex, 1);
            }
            if (culledIndex >= 0) {
                const removed = this.intrudersFiltered[culledIndex];
                this.intrudersFiltered.splice(culledIndex, 1);
                this.cleanUpIntruder(removed);
            }
        }
        /**
         * A callback which is called when the sim time changes.
         * @param simTime The current sim time.
         */
        onSimTimeChanged(simTime) {
            switch (this.operatingModeSub.get()) {
                case exports.TcasOperatingMode.Off:
                case exports.TcasOperatingMode.Standby:
                case exports.TcasOperatingMode.Failed:
                case exports.TcasOperatingMode.Test: // TODO: support TEST mode
                    return;
            }
            const realTime = Date.now();
            if (Math.abs(simTime - this.lastUpdateSimTime) < 1000 / this.simTimeUpdateFreq.get()
                || Math.abs(realTime - this.lastUpdateRealTime) < 1000 / this.realTimeUpdateFreq.get()) {
                return;
            }
            this.doUpdate(simTime);
            this.lastUpdateSimTime = simTime;
            this.lastUpdateRealTime = realTime;
        }
        /**
         * Executes an update.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        doUpdate(simTime) {
            this.updateSensitivity();
            this.updateIntruderPredictions(simTime);
            this.updateIntruderArrays();
            this.updateFilteredIntruderAlertLevels(simTime);
            this.updateResolutionAdvisory(simTime);
        }
        /**
         * Updates the TCA predictions for all intruders tracked by this system.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        updateIntruderPredictions(simTime) {
            this.ownAirplane.update(simTime);
            const len = this.intrudersSorted.length;
            for (let i = 0; i < len; i++) {
                const intruder = this.intrudersSorted[i];
                const sensitivity = this.sensitivity.selectParameters(intruder);
                intruder.updatePrediction(simTime, this.ownAirplane, sensitivity);
            }
        }
        /**
         * Updates the arrays of intruders tracked by this system.
         */
        updateIntruderArrays() {
            this.intrudersSorted.sort(this.intruderComparator.bind(this));
            const oldCulled = this.intrudersFiltered;
            this.intrudersFiltered = [];
            const len = this.intrudersSorted.length;
            for (let i = 0; i < len && this.intrudersFiltered.length < this.maxIntruderCount.get(); i++) {
                const intruder = this.intrudersSorted[i];
                if (intruder.isPredictionValid && this.filterIntruder(intruder)) {
                    this.intrudersFiltered.push(intruder);
                    if (!oldCulled.includes(intruder)) {
                        this.initIntruder(intruder);
                    }
                }
                else {
                    if (oldCulled.includes(intruder)) {
                        this.cleanUpIntruder(intruder);
                    }
                }
            }
        }
        /**
         * Filters an intruder.
         * @param intruder An intruder.
         * @returns Whether the intruder should be tracked by this TCAS.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        filterIntruder(intruder) {
            return true;
        }
        /**
         * Updates the alert levels for all intruders tracked by this system that have not been filtered out.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        updateFilteredIntruderAlertLevels(simTime) {
            let taCount = 0, raCount = 0;
            const len = this.intrudersFiltered.length;
            for (let i = 0; i < len; i++) {
                const intruder = this.intrudersFiltered[i];
                this.updateIntruderAlertLevel(simTime, intruder);
                switch (intruder.alertLevel.get()) {
                    case exports.TcasAlertLevel.TrafficAdvisory:
                        taCount++;
                        break;
                    case exports.TcasAlertLevel.ResolutionAdvisory:
                        raCount++;
                        break;
                }
            }
            this.eventPublisher.pub('tcas_ta_intruder_count', taCount, false, true);
            this.eventPublisher.pub('tcas_ra_intruder_count', raCount, false, true);
        }
        /**
         * Updates an intruder's alert level.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         */
        updateIntruderAlertLevel(simTime, intruder) {
            const currentAlertLevel = intruder.alertLevel.get();
            if (intruder.tcaRA.isValid && intruder.tcaRA.isThreat) {
                if (this.canIssueResolutionAdvisory(simTime, intruder)) {
                    intruder.alertLevel.set(exports.TcasAlertLevel.ResolutionAdvisory);
                    return;
                }
                else if (currentAlertLevel === exports.TcasAlertLevel.ResolutionAdvisory && !this.canCancelResolutionAdvisory(simTime, intruder)) {
                    return;
                }
            }
            if (currentAlertLevel === exports.TcasAlertLevel.ResolutionAdvisory
                && (!intruder.tcaRA.isValid || !intruder.tcaRA.isThreat)
                && !this.canCancelResolutionAdvisory(simTime, intruder)) {
                return;
            }
            if (intruder.tcaTA.isValid && intruder.tcaTA.isThreat) {
                if (this.canIssueTrafficAdvisory(simTime, intruder)) {
                    intruder.alertLevel.set(exports.TcasAlertLevel.TrafficAdvisory);
                    return;
                }
                else if (currentAlertLevel === exports.TcasAlertLevel.TrafficAdvisory && !this.canCancelTrafficAdvisory(simTime, intruder)) {
                    return;
                }
            }
            if (currentAlertLevel === exports.TcasAlertLevel.TrafficAdvisory
                && (!intruder.tcaTA.isValid || !intruder.tcaTA.isThreat)
                && !this.canCancelTrafficAdvisory(simTime, intruder)) {
                return;
            }
            if (intruder.isPredictionValid) {
                const parametersPA = this.sensitivity.selectParameters(intruder).parametersPA;
                const radius = parametersPA.protectedRadius;
                const height = parametersPA.protectedHeight;
                if (!radius.isNaN() && !height.isNaN() && this.canIssueProximityAdvisory(simTime, intruder)) {
                    intruder.predictSeparation(simTime, this.paSeparationCache.horizontal, this.paSeparationCache.vertical);
                    if (this.paSeparationCache.horizontal.compare(parametersPA.protectedRadius) <= 0
                        && this.paSeparationCache.vertical.compare(parametersPA.protectedHeight) <= 0) {
                        intruder.alertLevel.set(exports.TcasAlertLevel.ProximityAdvisory);
                        return;
                    }
                }
            }
            if (currentAlertLevel === exports.TcasAlertLevel.ProximityAdvisory && !this.canCancelProximityAdvisory(simTime, intruder)) {
                return;
            }
            intruder.alertLevel.set(exports.TcasAlertLevel.None);
        }
        /**
         * Checks whether a resolution advisory can be issued for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a resolution advisory can be issued for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canIssueResolutionAdvisory(simTime, intruder) {
            return this.operatingModeSub.get() === exports.TcasOperatingMode.TA_RA
                && intruder.tcaRA.isValid
                && intruder.tcaRA.tcpa.number > 0
                && this.resolutionAdvisoryHost.canIssueResolutionAdvisory(simTime, intruder);
        }
        /**
         * Checks whether a resolution advisory can be canceled for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a resolution advisory can be issued for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canCancelResolutionAdvisory(simTime, intruder) {
            return this.operatingModeSub.get() !== exports.TcasOperatingMode.TA_RA
                || this.resolutionAdvisoryHost.canCancelResolutionAdvisory(simTime);
        }
        /**
         * Checks whether a traffic advisory can be issued for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a traffic advisory can be issued for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canIssueTrafficAdvisory(simTime, intruder) {
            return true;
        }
        /**
         * Checks whether a traffic advisory can be canceled for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a traffic advisory can be canceled for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canCancelTrafficAdvisory(simTime, intruder) {
            return true;
        }
        /**
         * Checks whether a proximity advisory can be issued for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a proximity advisory can be issued for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canIssueProximityAdvisory(simTime, intruder) {
            return true;
        }
        /**
         * Checks whether a proximity advisory can be canceled for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder An intruder.
         * @returns Whether a proximity advisory can be canceled for the intruder.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        canCancelProximityAdvisory(simTime, intruder) {
            return true;
        }
        /**
         * Updates this TCAS's resolution advisory.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        updateResolutionAdvisory(simTime) {
            if (this.operatingModeSub.get() === exports.TcasOperatingMode.TA_RA) {
                this.resolutionAdvisoryHost.update(simTime, this.sensitivity.selectRAAlim(this.intrudersRA), this.intrudersRA);
            }
            else {
                this.resolutionAdvisoryHost.cancel(simTime);
            }
        }
        /**
         * Executes initialization code when an intruder is added.
         * @param intruder The newly added intruder.
         */
        initIntruder(intruder) {
            this.alertLevelSubs.set(intruder, intruder.alertLevel.sub(this.onAlertLevelChanged.bind(this, intruder)));
            this.eventPublisher.pub('tcas_intruder_added', intruder, false, false);
        }
        /**
         * Executes cleanup code when an intruder is removed.
         * @param intruder The intruder that was removed.
         */
        cleanUpIntruder(intruder) {
            var _a;
            if (intruder.alertLevel.get() === exports.TcasAlertLevel.ResolutionAdvisory) {
                this.intrudersRA.delete(intruder);
            }
            (_a = this.alertLevelSubs.get(intruder)) === null || _a === void 0 ? void 0 : _a.destroy();
            this.eventPublisher.pub('tcas_intruder_removed', intruder, false, false);
        }
        /**
         * A callback which is called when an intruder's alert level changes.
         * @param intruder The intruder whose alert level changed.
         */
        onAlertLevelChanged(intruder) {
            if (intruder.alertLevel.get() === exports.TcasAlertLevel.ResolutionAdvisory) {
                this.intrudersRA.add(intruder);
            }
            else {
                this.intrudersRA.delete(intruder);
            }
            this.eventPublisher.pub('tcas_intruder_alert_changed', intruder, false, false);
        }
    }
    Tcas.DEFAULT_RA_OPTIONS = {
        initialResponseTime: UnitType.SECOND.createNumber(5),
        initialAcceleration: UnitType.G_ACCEL.createNumber(0.25),
        subsequentResponseTime: UnitType.SECOND.createNumber(2.5),
        subsequentAcceleration: UnitType.G_ACCEL.createNumber(0.35)
    };
    /**
     * An airplane managed by TCAS.
     */
    class TcasAirplane {
        constructor() {
            this._position = new GeoPoint(0, 0);
            /** The position of this airplane at the time of the most recent update. */
            this.position = this._position.readonly;
            /** The altitude of this airplane at the time of the most recent update. */
            this._altitude = UnitType.FOOT.createNumber(0);
            this.altitude = this._altitude.readonly;
            this._groundTrack = 0;
            /** The ground speed of this airplane at the time of the most recent update. */
            this._groundSpeed = UnitType.KNOT.createNumber(0);
            this.groundSpeed = this._groundSpeed.readonly;
            /** The vertical speed of this airplane at the time of the most recent update. */
            this._verticalSpeed = UnitType.FPM.createNumber(0);
            this.verticalSpeed = this._verticalSpeed.readonly;
            /**
             * The 3D position vector of this airplane at the time of the last update. Each component is expressed in units of
             * meters. The coordinate system is an Euclidean approximation of the geodetic space around the own airplane such
             * that the z-coordinate represents orthometric height and the x- and y-coordinates represent an east-
             * counterclockwise equirectangular projection of latitude and longitude, with the origin at the location of the own
             * airplane.
             */
            this.positionVec = new Float64Array(3);
            /**
             * The 3D velocity vector of this airplane at the time of the last update. Each component is expressed in units of
             * meters per second. The coordinate system is defined the same as for position vectors.
             */
            this.velocityVec = new Float64Array(3);
            this.lastUpdateTime = 0;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The true ground track of this airplane at the time of the most recent update. */
        get groundTrack() {
            return this._groundTrack;
        }
    }
    /**
     * The own airplane managed by TCAS.
     */
    class OwnAirplane extends TcasAirplane {
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether this airplane is on the ground. */
        get isOnGround() {
            return this._isOnGround;
        }
        /**
         * Constructor.
         * @param subs Subscribables which provide data related to this airplane.
         */
        constructor(subs) {
            super();
            this.subs = subs;
            /** The radar altitude of this airplane at the time of the most recent update. */
            this._radarAltitude = UnitType.FOOT.createNumber(0);
            this.radarAltitude = this._radarAltitude.readonly;
            this._isOnGround = false;
        }
        /**
         * Calculates the predicted 3D position vector of this airplane at a specified time based on the most recent
         * available data. Each component of the vector is expressed in units of meters, and the origin lies at the most
         * recent updated position of this airplane.
         * @param simTime The sim time at which to calculate the position, as a UNIX timestamp in milliseconds.
         * @param out A Float64Array object to which to write the result.
         * @returns The predicted position vector of this airplane at the specified time.
         */
        predictPosition(simTime, out) {
            const dt = (simTime - this.lastUpdateTime) / 1000;
            return Vec3Math.add(this.positionVec, Vec3Math.multScalar(this.velocityVec, dt, out), out);
        }
        /**
         * Updates this airplane's position and velocity data.
         * @param simTime The current sim time, as a UNIX millisecond timestamp.
         */
        update(simTime) {
            this.updateParameters();
            this.updateVectors();
            this.lastUpdateTime = simTime;
        }
        /**
         * Updates this airplane's position, altitude, ground track, ground speed, vertical speed, and whether it is on the ground.
         */
        updateParameters() {
            this._position.set(this.subs.position.get());
            this._altitude.set(this.subs.altitude.get());
            this._groundTrack = this.subs.groundTrack.get();
            this._groundSpeed.set(this.subs.groundSpeed.get());
            this._verticalSpeed.set(this.subs.verticalSpeed.get());
            this._radarAltitude.set(this.subs.radarAltitude.get());
            this._isOnGround = this.subs.isOnGround.get();
        }
        /**
         * Updates this airplane's position and velocity vectors.
         */
        updateVectors() {
            Vec2Math.setFromPolar(this._groundSpeed.asUnit(UnitType.MPS), (90 - this._groundTrack) * Avionics.Utils.DEG2RAD, this.velocityVec);
            const verticalVelocity = this._verticalSpeed.asUnit(UnitType.MPS);
            this.velocityVec[2] = verticalVelocity;
        }
    }
    /**
     * An abstract implementation of {@link TcasIntruder}.
     */
    class AbstractTcasIntruder extends TcasAirplane {
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether there is a valid prediction for time of closest approach between this intruder and own airplane. */
        get isPredictionValid() {
            return this._isPredictionValid;
        }
        /**
         * Constructor.
         * @param contact The traffic contact associated with this intruder.
         */
        constructor(contact) {
            super();
            this.contact = contact;
            this.alertLevel = Subject.create(exports.TcasAlertLevel.None);
            /** The 3D position vector of this intruder relative to own airplane. */
            this.relativePositionVec = new Float64Array(3);
            /** The 3D velocity vector of this intruder relative to own airplane. */
            this.relativeVelocityVec = new Float64Array(3);
            this._isPredictionValid = false;
            /** @inheritdoc */
            this.tcaTA = new TcasTcaPredictionClass(this);
            /** @inheritdoc */
            this.tcaRA = new TcasTcaPredictionClass(this);
        }
        /** @inheritdoc */
        predictDisplacement(simTime, out) {
            if (!this._isPredictionValid) {
                return Vec3Math.set(NaN, NaN, NaN, out);
            }
            const dt = (simTime - this.contact.lastContactTime) / 1000;
            return Vec3Math.add(this.relativePositionVec, Vec3Math.multScalar(this.relativeVelocityVec, dt, out), out);
        }
        /** @inheritdoc */
        predictSeparation(simTime, horizontalOut, verticalOut) {
            if (!this._isPredictionValid) {
                horizontalOut.set(NaN);
                verticalOut.set(NaN);
                return;
            }
            const displacement = this.predictDisplacement(simTime, AbstractTcasIntruder.vec3Cache[0]);
            AbstractTcasIntruder.displacementToHorizontalSeparation(displacement, horizontalOut);
            AbstractTcasIntruder.displacementToVerticalSeparation(displacement, verticalOut);
        }
        /**
         * Updates this intruder's predicted TCA and related data.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplane Own airplane.
         * @param sensitivity The TCAS sensitivity parameters to use when calculating predictions.
         */
        updatePrediction(simTime, ownAirplane, sensitivity) {
            this.updateParameters(simTime, ownAirplane);
            if (this.isPredictionValid) {
                const taParams = sensitivity.parametersTA;
                const raParams = sensitivity.parametersRA;
                this.tcaTA.update(simTime, taParams.tau, taParams.protectedRadius, taParams.protectedHeight, taParams.hmd);
                this.tcaRA.update(simTime, raParams.tau, raParams.protectedRadius, raParams.protectedHeight, raParams.hmd);
            }
            else {
                this.invalidatePredictions();
            }
            this.lastUpdateTime = simTime;
        }
        /**
         * Updates this intruder's position and velocity data.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplane The own airplane.
         */
        updateParameters(simTime, ownAirplane) {
            if (isNaN(this.contact.groundTrack) || this.contact.groundSpeed.compare(AbstractTcasIntruder.MIN_GROUND_SPEED) < 0) {
                this._isPredictionValid = false;
                this._position.set(NaN, NaN);
                this._altitude.set(NaN);
                this._groundTrack = NaN;
                this._groundSpeed.set(NaN);
                this._verticalSpeed.set(NaN);
                Vec3Math.set(NaN, NaN, NaN, this.positionVec);
                Vec3Math.set(NaN, NaN, NaN, this.velocityVec);
                Vec3Math.set(NaN, NaN, NaN, this.relativePositionVec);
                Vec3Math.set(NaN, NaN, NaN, this.relativeVelocityVec);
            }
            else {
                this.updatePosition(simTime, ownAirplane);
                this.updateVelocity(ownAirplane);
                this._groundSpeed.set(this.contact.groundSpeed);
                this._verticalSpeed.set(this.contact.verticalSpeed);
                this._isPredictionValid = true;
            }
        }
        /**
         * Updates this intruder's position.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplane The own airplane.
         */
        updatePosition(simTime, ownAirplane) {
            this.contact.predict(simTime, this._position, this._altitude);
            this._groundTrack = this._position.equals(this.contact.lastPosition) ? this.contact.groundTrack : this._position.bearingFrom(this.contact.lastPosition);
            const distance = UnitType.GA_RADIAN.convertTo(this._position.distance(ownAirplane.position), UnitType.METER);
            const bearing = ownAirplane.position.bearingTo(this._position);
            Vec2Math.setFromPolar(distance, (90 - bearing) * Avionics.Utils.DEG2RAD, this.positionVec);
            const verticalPosition = this._altitude.asUnit(UnitType.METER) - ownAirplane.altitude.asUnit(UnitType.METER);
            this.positionVec[2] = verticalPosition;
            Vec3Math.sub(this.positionVec, ownAirplane.positionVec, this.relativePositionVec);
        }
        /**
         * Updates this intruder's velocity.
         * @param ownAirplane The own airplane.
         */
        updateVelocity(ownAirplane) {
            Vec2Math.setFromPolar(this.contact.groundSpeed.asUnit(UnitType.MPS), (90 - this.contact.groundTrack) * Avionics.Utils.DEG2RAD, this.velocityVec);
            const verticalVelocity = this.contact.verticalSpeed.asUnit(UnitType.MPS);
            this.velocityVec[2] = verticalVelocity;
            Vec3Math.sub(this.velocityVec, ownAirplane.velocityVec, this.relativeVelocityVec);
        }
        /**
         * Invalidates this intruder's predicted TCA and related data.
         */
        invalidatePredictions() {
            this.tcaTA.invalidate();
            this.tcaRA.invalidate();
        }
        /**
         * Converts a 3D displacement vector to a horizontal separation distance.
         * @param displacement A displacement vector, in meters.
         * @param out A NumberUnit object to which to write the result.
         * @returns The horizontal separation distance corresponding to the displacement vector.
         */
        static displacementToHorizontalSeparation(displacement, out) {
            return out.set(Math.hypot(displacement[0], displacement[1]), UnitType.METER);
        }
        /**
         * Converts a 3D displacement vector to a vertical separation distance.
         * @param displacement A displacement vector, in meters.
         * @param out A NumberUnit object to which to write the result.
         * @returns The vertical separation distance corresponding to the displacement vector.
         */
        static displacementToVerticalSeparation(displacement, out) {
            return out.set(Math.abs(displacement[2]), UnitType.METER);
        }
    }
    AbstractTcasIntruder.MIN_GROUND_SPEED = UnitType.KNOT.createNumber(30);
    AbstractTcasIntruder.vec3Cache = [new Float64Array(3), new Float64Array(3)];
    /**
     * An default implementation of {@link TcasIntruder}.
     */
    class DefaultTcasIntruder extends AbstractTcasIntruder {
    }
    /**
     * A time-of-closest-approach prediction made by TCAS.
     */
    class TcasTcaPredictionClass {
        /** @inheritdoc */
        get isValid() {
            return this._isValid;
        }
        /** @inheritdoc */
        get time() {
            return this._time;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** @inheritdoc */
        get isThreat() {
            return this._isThreat;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** @inheritdoc */
        get cpaNorm() {
            return this._cpaNorm;
        }
        /**
         * Constructor.
         * @param intruder The intruder associated with this prediction.
         */
        constructor(intruder) {
            this.intruder = intruder;
            this._isValid = false;
            this._time = NaN;
            this._tcpa = UnitType.SECOND.createNumber(NaN);
            /** @inheritdoc */
            this.tcpa = this._tcpa.readonly;
            this._tcoa = UnitType.SECOND.createNumber(NaN);
            /** @inheritdoc */
            this.tcoa = this._tcoa.readonly;
            this._isThreat = false;
            /** @inheritdoc */
            this.cpaDisplacement = new Float64Array(3);
            this._cpaHorizontalSep = UnitType.NMILE.createNumber(0);
            /** @inheritdoc */
            this.cpaHorizontalSep = this._cpaHorizontalSep.readonly;
            this._cpaVerticalSep = UnitType.FOOT.createNumber(0);
            /** @inheritdoc */
            this.cpaVerticalSep = this._cpaVerticalSep.readonly;
            this._cpaNorm = NaN;
        }
        /**
         * Updates the time-to-closest-approach (TCA) and related data of this intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param tau The maximum lookahead time.
         * @param dmod The radius of the own airplane's protected volume.
         * @param zthr The half-height of the own airplane's protected volume.
         * @param hmd The distance threshold for the horizontal miss distance filter. If not defined, the HMD filter will
         * not be applied.
         */
        update(simTime, tau, dmod, zthr, hmd) {
            var _a;
            this._time = simTime;
            if (tau.isNaN() || dmod.isNaN() || zthr.isNaN() || ((_a = hmd === null || hmd === void 0 ? void 0 : hmd.isNaN()) !== null && _a !== void 0 ? _a : false)) {
                this.invalidate();
                return;
            }
            // Source: Munoz, CA and Narkawicz, AJ. "A TCAS-II Resolution Advisory Detection Algorithm."
            // https://ntrs.nasa.gov/api/citations/20140002736/downloads/20140002736.pdf
            const tauSeconds = tau.asUnit(UnitType.SECOND);
            const s = this.intruder.relativePositionVec;
            const v = this.intruder.relativeVelocityVec;
            const sHoriz = Vec2Math.set(s[0], s[1], TcasTcaPredictionClass.vec2Cache[0]);
            const vHoriz = Vec2Math.set(v[0], v[1], TcasTcaPredictionClass.vec2Cache[1]);
            const h = zthr.asUnit(UnitType.METER);
            const r = dmod.asUnit(UnitType.METER);
            const vHorizSquared = Vec2Math.dot(vHoriz, vHoriz);
            const sHorizSquared = Vec2Math.dot(sHoriz, sHoriz);
            const dotSHorizVHoriz = Vec2Math.dot(vHoriz, sHoriz);
            const rSquared = r * r;
            // Time to closest horizontal approach
            const tcpa = vHorizSquared === 0 ? 0 : -dotSHorizVHoriz / vHorizSquared;
            // Modified tau
            const tauMod = dotSHorizVHoriz >= 0 ? Infinity : (rSquared - sHorizSquared) / dotSHorizVHoriz;
            // Time to co-altitude
            const tcoa = -s[2] / v[2];
            const isHorizontalThreat = sHorizSquared <= rSquared || tauMod <= tauSeconds;
            const isVerticalThreat = Math.abs(s[2]) <= h || (tcoa >= 0 && tcoa <= tauSeconds);
            let passHmdFilter = true;
            if (hmd !== undefined && isHorizontalThreat && isVerticalThreat) {
                const d = hmd.asUnit(UnitType.METER);
                const dSquared = d * d;
                if (vHorizSquared === 0) {
                    passHmdFilter = sHorizSquared <= dSquared;
                }
                else {
                    const delta = dSquared * vHorizSquared - Vec2Math.dot(sHoriz, Vec2Math.normal(vHoriz, TcasTcaPredictionClass.vec2Cache[1]));
                    if (delta < 0) {
                        passHmdFilter = false;
                    }
                    else {
                        const a = vHorizSquared;
                        const b = 2 * dotSHorizVHoriz;
                        const c = sHorizSquared - dSquared;
                        const discriminant = b * b - 4 * a * c;
                        if (discriminant < 0) {
                            passHmdFilter = false;
                        }
                        else {
                            passHmdFilter = (-b + Math.sqrt(discriminant)) / (2 * a) >= 0;
                        }
                    }
                }
            }
            this._isThreat = isHorizontalThreat && isVerticalThreat && passHmdFilter;
            this._tcpa.set(tcpa);
            this._tcoa.set(tcoa);
            TcasTcaPredictionClass.calculateDisplacementVector(s, v, tcpa, this.cpaDisplacement);
            AbstractTcasIntruder.displacementToHorizontalSeparation(this.cpaDisplacement, this._cpaHorizontalSep);
            AbstractTcasIntruder.displacementToVerticalSeparation(this.cpaDisplacement, this._cpaVerticalSep);
            this._cpaNorm = TcasTcaPredictionClass.calculateCylindricalNorm(this.cpaDisplacement, r, h);
            this._isValid = true;
        }
        /**
         * Invalidates this intruder's predicted TCA and related data.
         */
        invalidate() {
            this._isValid = false;
            this._isThreat = false;
            this._tcpa.set(NaN);
            this._tcoa.set(NaN);
            Vec3Math.set(NaN, NaN, NaN, this.cpaDisplacement);
            this._cpaHorizontalSep.set(NaN);
            this._cpaVerticalSep.set(NaN);
            this._cpaNorm = NaN;
        }
        /**
         * Calculates a time-offset displacement vector given an initial displacement, a velocity vector, and elapsed time.
         * @param initial The initial displacement vector.
         * @param velocity A velocity vector.
         * @param elapsedTime The elapsed time.
         * @param out A Float64Array object to which to write the result.
         * @returns The time-offset displacement vector.
         */
        static calculateDisplacementVector(initial, velocity, elapsedTime, out) {
            return Vec3Math.add(initial, Vec3Math.multScalar(velocity, elapsedTime, out), out);
        }
        /**
         * Calculates a cylindrical norm.
         * @param vector A displacement vector.
         * @param radius The radius of the protected zone.
         * @param halfHeight The half-height of the protected zone.
         * @returns A cylindrical norm.
         */
        static calculateCylindricalNorm(vector, radius, halfHeight) {
            const horizLength = Math.hypot(vector[0], vector[1]);
            return Math.max(Math.abs(vector[2]) / halfHeight, horizLength / radius);
        }
    }
    TcasTcaPredictionClass.vec2Cache = [new Float64Array(2), new Float64Array(2), new Float64Array(2)];
    /**
     * An implementation of {@link TcasResolutionAdvisoryHost}.
     */
    class TcasResolutionAdvisoryHostClass {
        /** @inheritdoc */
        get flags() {
            return this._flags;
        }
        /** @inheritdoc */
        get primaryType() {
            return this._primaryType;
        }
        /** @inheritdoc */
        get secondaryType() {
            return this._secondaryType;
        }
        /** @inheritdoc */
        get primaryFlags() {
            return this._primaryFlags;
        }
        /** @inheritdoc */
        get secondaryFlags() {
            return this._secondaryFlags;
        }
        /**
         * Constructor.
         * @param bus The event bus.
         * @param options Options to adjust how the host should calculate resolution advisories.
         * @param ownAirplane The own airplane.
         */
        constructor(bus, options, ownAirplane) {
            this.options = options;
            this.ownAirplane = ownAirplane;
            this.initialResponseTimeSeconds = this.options.initialResponseTime.asUnit(UnitType.SECOND);
            this.initialAccelMps = this.options.initialAcceleration.asUnit(UnitType.MPS_PER_SEC);
            this.subsequentResponseTimeSeconds = this.options.subsequentResponseTime.asUnit(UnitType.SECOND);
            this.subsequentAccelMps = this.options.subsequentAcceleration.asUnit(UnitType.MPS_PER_SEC);
            this.intruders = new Set();
            this.intruderArray = [];
            this._maxVerticalSpeed = UnitType.FPM.createNumber(NaN);
            /** @inheritdoc */
            this.maxVerticalSpeed = this._maxVerticalSpeed.readonly;
            this._minVerticalSpeed = UnitType.FPM.createNumber(NaN);
            /** @inheritdoc */
            this.minVerticalSpeed = this._minVerticalSpeed.readonly;
            this._flags = 0;
            this._primaryType = exports.TcasResolutionAdvisoryType.Clear;
            this._secondaryType = null;
            this._primaryFlags = 0;
            this._secondaryFlags = 0;
            this.vsConstraints = [];
            this.lastStateChangeTime = 0;
            this.stateChangeDelay = 0;
            this.isInitial = true;
            this.senseReversalCount = 0;
            this.publisher = bus.getPublisher();
        }
        /**
         * Checks whether this host can issue a resolution advisory for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param intruder The query intruder.
         * @returns Whether this host can issue a resolution advisory for an intruder.
         */
        canIssueResolutionAdvisory(simTime, intruder) {
            // Inhibit resolution advisories for intruders whose time to closest horizontal approach is less than or equal to
            // zero (indicating the closest point of approach has already been passed) and whose horizontal separation from the
            // own airplane is increasing.
            if (intruder.tcaRA.tcpa.number <= 0 && Vec2Math.dot(intruder.relativeVelocityVec, intruder.relativePositionVec) > 0) {
                return false;
            }
            if (this._primaryType !== exports.TcasResolutionAdvisoryType.Clear) {
                return true;
            }
            const lastStateChangeTime = Math.min(simTime, this.lastStateChangeTime);
            return simTime - lastStateChangeTime >= this.stateChangeDelay;
        }
        /**
         * Checks whether this host can cancel a resolution advisory for an intruder.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @returns Whether this host can cancel a resolution advisory for an intruder.
         */
        canCancelResolutionAdvisory(simTime) {
            if (this._primaryType === exports.TcasResolutionAdvisoryType.Clear) {
                return true;
            }
            const lastStateChangeTime = Math.min(simTime, this.lastStateChangeTime);
            return simTime - lastStateChangeTime >= this.stateChangeDelay;
        }
        /**
         * Updates this host's resolution advisory.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param alim The required vertical separation between own airplane and intruders.
         * @param intruders The set of active intruders to be tracked by the resolution advisory.
         */
        update(simTime, alim, intruders) {
            if (this.intruderArray.length === 0 && intruders.size === 0) {
                return;
            }
            if (simTime < this.lastStateChangeTime) {
                this.lastStateChangeTime = simTime;
            }
            if (simTime - this.lastStateChangeTime < this.stateChangeDelay) {
                return;
            }
            if (intruders.size === 0) {
                this.cancel(simTime);
            }
            else {
                const isInitial = this._primaryType === exports.TcasResolutionAdvisoryType.Clear;
                const ownAirplaneVsMps = this.ownAirplane.verticalSpeed.asUnit(UnitType.MPS);
                const alimMeters = alim.asUnit(UnitType.METER);
                const responseTimeSeconds = this.isInitial ? this.initialResponseTimeSeconds : this.subsequentResponseTimeSeconds;
                const responseTimeSecondsRemaining = isInitial
                    ? responseTimeSeconds
                    : MathUtils.clamp(responseTimeSeconds - (simTime - this.lastStateChangeTime) / 1000, 0, responseTimeSeconds);
                const accelMps = this.isInitial ? this.initialAccelMps : this.subsequentAccelMps;
                this.updateIntruders(intruders);
                this.updateVsConstraints(alimMeters, responseTimeSecondsRemaining, accelMps);
                if (isInitial) {
                    this.selectInitialState(simTime, ownAirplaneVsMps);
                }
                else {
                    if (BitFlags.isAny(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Descend)) {
                        this.updatePositive(simTime);
                    }
                    else if (this._secondaryType === null) {
                        this.updateNegative(simTime, ownAirplaneVsMps);
                    }
                    else {
                        this.updateComposite(simTime, ownAirplaneVsMps);
                    }
                }
            }
        }
        /**
         * Updates this host's resolution advisory's array of active intruders.
         * @param intruders The set of active intruders to be tracked by the resolution advisory.
         */
        updateIntruders(intruders) {
            // Remove one sense reversal used for every intruder that was removed from the RA.
            for (let i = 0; i < this.intruderArray.length; i++) {
                if (!intruders.has(this.intruderArray[i])) {
                    this.senseReversalCount = Math.max(this.senseReversalCount - 1, 0);
                }
            }
            this.intruders.clear();
            this.intruderArray.length = 0;
            for (const intruder of intruders) {
                this.intruders.add(intruder);
                this.intruderArray.push(intruder);
            }
            this.intruderArray.sort(TcasResolutionAdvisoryHostClass.INTRUDER_SORT_FUNC);
        }
        /**
         * Updates the vertical speed constraints associated with the intruders participating in this host's current
         * resolution advisory.
         * @param alimMeters The value of ALIM (the minimum desired vertical separation between the own airplane and an
         * intruder at time of closest approach), in meters.
         * @param responseTimeSeconds The predicted amount of time, in seconds, for the own airplane to respond to the most
         * recently issued resolution advisory, relative to the present.
         * @param accelMps The predicted vertical acceleration, in meters per second squared, of the own airplane when
         * responding to the most recently issued resolution advisory.
         */
        updateVsConstraints(alimMeters, responseTimeSeconds, accelMps) {
            var _a;
            var _b;
            const ownAirplaneVsMps = this.ownAirplane.verticalSpeed.asUnit(UnitType.MPS);
            for (let i = 0; i < this.intruderArray.length; i++) {
                const intruder = this.intruderArray[i];
                let above = -Infinity;
                let below = Infinity;
                if (intruder.tcaRA.isValid) {
                    const t0 = intruder.tcaRA.time;
                    const tcpaSeconds = intruder.tcaRA.tcpa.asUnit(UnitType.SECOND);
                    const tcpaTime = intruder.tcaRA.time + tcpaSeconds * 1000;
                    const ownAirplaneAltMeters = this.ownAirplane.predictPosition(t0, TcasResolutionAdvisoryHostClass.vec3Cache[0])[2];
                    const intruderTcaAltMeters = ownAirplaneAltMeters + tcpaSeconds * ownAirplaneVsMps + intruder.tcaRA.cpaDisplacement[2];
                    const ownAirplaneAltTcaMeters = this.ownAirplane.predictPosition(tcpaTime, TcasResolutionAdvisoryHostClass.vec3Cache[0])[2];
                    // The altitude own airplane needs to be above in order to pass above the intruder with ALIM vertical separation at CPA.
                    const aboveAltTargetMeters = intruderTcaAltMeters + alimMeters;
                    // The altitude own airplane needs to be below in order to pass below the intruder with ALIM vertical separation at CPA.
                    const belowAltTargetMeters = intruderTcaAltMeters - alimMeters;
                    const aboveAlimSense = Math.sign(aboveAltTargetMeters - ownAirplaneAltTcaMeters);
                    const belowAlimSense = Math.sign(belowAltTargetMeters - ownAirplaneAltTcaMeters);
                    if (aboveAlimSense === 1) {
                        // Own airplane needs to adjust vertical speed in the positive direction in order to pass above the intruder with
                        // ALIM vertical separation at CPA.
                        above = responseTimeSeconds < tcpaSeconds
                            ? TcasResolutionAdvisoryHostClass.calculateVSToTargetAlt(tcpaSeconds, ownAirplaneAltMeters, ownAirplaneVsMps, responseTimeSeconds, accelMps, aboveAltTargetMeters)
                            : NaN;
                        if (isNaN(above)) {
                            above = Infinity;
                        }
                    }
                    else {
                        // Own airplane is already on track to pass above the intruder with ALIM vertical separation at CPA.
                        above = tcpaSeconds > 0
                            ? (aboveAltTargetMeters - ownAirplaneAltMeters) / tcpaSeconds
                            : -Infinity;
                    }
                    if (belowAlimSense === -1) {
                        // Own airplane needs to adjust vertical speed in the negative direction in order to pass below the intruder with
                        // ALIM vertical separation at CPA
                        below = responseTimeSeconds < tcpaSeconds
                            ? TcasResolutionAdvisoryHostClass.calculateVSToTargetAlt(tcpaSeconds, ownAirplaneAltMeters, ownAirplaneVsMps, responseTimeSeconds, accelMps, belowAltTargetMeters)
                            : NaN;
                        if (isNaN(below)) {
                            below = -Infinity;
                        }
                    }
                    else {
                        // Own airplane is already on track to pass below the intruder with ALIM vertical separation at CPA.
                        below = tcpaSeconds > 0
                            ? (belowAltTargetMeters - ownAirplaneAltMeters) / tcpaSeconds
                            : Infinity;
                    }
                }
                const constraint = (_a = (_b = this.vsConstraints)[i]) !== null && _a !== void 0 ? _a : (_b[i] = { above: 0, below: 0 });
                constraint.above = above;
                constraint.below = below;
            }
            this.vsConstraints.length = this.intruderArray.length;
        }
        /**
         * Selects an initial state for a new resolution advisory.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplaneVsMps The current vertical speed of the own airplane, in meters per second.
         */
        selectInitialState(simTime, ownAirplaneVsMps) {
            // We need to first select either upward or downward sense.
            // The minimum vertical speed own airplane can target while still achieving ALIM separation ABOVE all intruders
            let minUpSenseVsMps = this.getUpSenseRequiredMinVs();
            // The maximum vertical speed own airplane can target while still achieving ALIM separation BELOW all intruders
            let maxDownSenseVsMps = this.getDownSenseRequiredMaxVs();
            // The strongest initial upward and downward sense RAs are CLIMB and DESCEND, respectively (or their crossing/maintain
            // variants, but all have the same vertical speed target).
            let doesUpSenseAchieveAlim = minUpSenseVsMps <= TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS;
            let doesDownSenseAchieveAlim = maxDownSenseVsMps >= -TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS;
            const doesUpSenseRequireClimb = minUpSenseVsMps > 0;
            const doesDownSenseRequireDescend = maxDownSenseVsMps < 0;
            const canClimb = this.options.allowClimb(simTime);
            const canDescend = this.options.allowDescend(simTime);
            const isUpSenseInhibited = doesUpSenseRequireClimb && !canClimb;
            const isDownSenseInhibited = doesDownSenseRequireDescend && !canDescend;
            minUpSenseVsMps = isUpSenseInhibited ? 0 : minUpSenseVsMps;
            maxDownSenseVsMps = isDownSenseInhibited ? 0 : maxDownSenseVsMps;
            let sense;
            let senseIsCrossing = false;
            let doesSenseAchieveAlim = false;
            let senseRequiredVs = 0;
            // If one sense RA achieves ALIM separation and the other does not, choose the one that achieves ALIM separation.
            if (doesUpSenseAchieveAlim && !doesDownSenseAchieveAlim) {
                sense = 1;
            }
            else if (!doesUpSenseAchieveAlim && doesDownSenseAchieveAlim) {
                sense = -1;
            }
            else {
                // If both or neither sense RA achieves ALIM separation, choose the non-crossing sense. Only the closest intruder
                // is evaluated, since in a multi-intruder RA there is the opportunity to reverse sense with each intruder.
                const closestIntruder = this.intruderArray[0];
                const t0 = closestIntruder.tcaRA.time;
                const tcpaSeconds = closestIntruder.tcaRA.tcpa.asUnit(UnitType.SECOND);
                const ownAirplaneAltMeters = this.ownAirplane.predictPosition(t0, TcasResolutionAdvisoryHostClass.vec3Cache[0])[2];
                const intruderTcpaAltMeters = ownAirplaneAltMeters + tcpaSeconds * ownAirplaneVsMps + closestIntruder.tcaRA.cpaDisplacement[2];
                const crossingSense = Math.sign(intruderTcpaAltMeters - ownAirplaneAltMeters);
                if (crossingSense === -1 && !isUpSenseInhibited) {
                    sense = 1;
                }
                else if (crossingSense === 1 && !isDownSenseInhibited) {
                    sense = -1;
                }
                else {
                    // If neither sense is crossing or the non-crossing sense is inhibited, choose the one that is not inhibited.
                    if (!isUpSenseInhibited && isDownSenseInhibited) {
                        sense = 1;
                    }
                    else if (isUpSenseInhibited && !isDownSenseInhibited) {
                        sense = -1;
                    }
                    else {
                        // If neither or both senses are inhibited, choose the one that gives the greatest potential separation at
                        // CPA (i.e. the one that requires the least change in vertical speed). Again, only the closest intruder is evaluated.
                        if (Math.abs(minUpSenseVsMps - ownAirplaneVsMps) < Math.abs(maxDownSenseVsMps - ownAirplaneVsMps)) {
                            sense = 1;
                        }
                        else {
                            sense = -1;
                        }
                    }
                    senseIsCrossing = sense === crossingSense;
                }
            }
            doesUpSenseAchieveAlim = isUpSenseInhibited ? false : doesUpSenseAchieveAlim;
            doesDownSenseAchieveAlim = isDownSenseInhibited ? false : doesDownSenseAchieveAlim;
            doesSenseAchieveAlim = sense === 1 ? doesUpSenseAchieveAlim : doesDownSenseAchieveAlim;
            senseRequiredVs = sense === 1 ? minUpSenseVsMps : maxDownSenseVsMps;
            // For multi-intruder RAs, there is the option to choose an initial composite RA which combines two negative RAs of
            // opposite senses.
            if (this.intruderArray.length > 1) {
                // Because the composite RA always includes a vertical speed of 0 within its vertical speed target range, it
                // achieves ALIM separation from all intruders if and only if a vertical speed of 0 does.
                const doesCompositeAchieveAlim = this.vsConstraints.every(constraint => constraint.above <= 0 || constraint.below >= 0);
                // If the composite RA achieves ALIM separation from all intruders and the selected up/down sense RA does not,
                // choose the composite RA.
                if (doesCompositeAchieveAlim && !doesSenseAchieveAlim) {
                    sense = 0;
                    doesSenseAchieveAlim = doesCompositeAchieveAlim;
                }
                else if (doesCompositeAchieveAlim === doesSenseAchieveAlim) {
                    // If both or neither the composite RA or the selected up/down sense RA achieves ALIM separation from all
                    // intruders, choose the composite RA if the up/down sense RA is crossing.
                    if (senseIsCrossing) {
                        sense = 0;
                        doesSenseAchieveAlim = doesCompositeAchieveAlim;
                    }
                    else {
                        // If both or neither the composite RA or the selected up/down sense RA is crossing, choose the one that
                        // provides the greatest potential vertical separation at CPA. Again, only the closest intruder is
                        // evaluated.
                        if (Math.abs(ownAirplaneVsMps) < Math.abs(senseRequiredVs - ownAirplaneVsMps)) {
                            sense = 0;
                            doesSenseAchieveAlim = doesCompositeAchieveAlim;
                        }
                    }
                }
            }
            // Now that we have selected the sense, we need to choose the appropriate initial RA type.
            let primaryType;
            let secondaryType = null;
            switch (sense) {
                case 0: // Composite RA
                    if (doesSenseAchieveAlim) {
                        // Find the vertical speed limits required to achieve ALIM separation.
                        const maxVsMps = this.getCompositeRequiredMaxVs();
                        const minVsMps = this.getCompositeRequiredMinVs();
                        if (ownAirplaneVsMps > maxVsMps) {
                            // Downward sense, corrective
                            primaryType = exports.TcasResolutionAdvisoryType.ReduceClimb;
                            secondaryType = TcasResolutionAdvisoryHostClass.getDoNotDescendType(minVsMps);
                        }
                        else if (ownAirplaneVsMps < minVsMps) {
                            // Upward sense, corrective
                            primaryType = exports.TcasResolutionAdvisoryType.ReduceDescent;
                            secondaryType = TcasResolutionAdvisoryHostClass.getDoNotClimbType(maxVsMps);
                        }
                        else {
                            // Preventative
                            primaryType = TcasResolutionAdvisoryHostClass.getDoNotDescendType(minVsMps);
                            secondaryType = TcasResolutionAdvisoryHostClass.getDoNotClimbType(maxVsMps);
                        }
                    }
                    else {
                        primaryType = exports.TcasResolutionAdvisoryType.ReduceDescent;
                        secondaryType = exports.TcasResolutionAdvisoryType.DoNotClimb0;
                    }
                    break;
                case 1: // Upward sense single RA
                    if (ownAirplaneVsMps < minUpSenseVsMps) {
                        // Corrective
                        if (minUpSenseVsMps > 0) {
                            // Positive
                            primaryType = senseIsCrossing ? exports.TcasResolutionAdvisoryType.CrossingClimb : exports.TcasResolutionAdvisoryType.Climb;
                        }
                        else {
                            // Negative
                            primaryType = exports.TcasResolutionAdvisoryType.ReduceDescent;
                        }
                    }
                    else {
                        // Preventative
                        if (minUpSenseVsMps > 0) {
                            // Positive
                            primaryType = senseIsCrossing ? exports.TcasResolutionAdvisoryType.CrossingMaintainClimb : exports.TcasResolutionAdvisoryType.MaintainClimb;
                        }
                        else {
                            // Negative
                            primaryType = TcasResolutionAdvisoryHostClass.getDoNotDescendType(minUpSenseVsMps);
                        }
                    }
                    break;
                case -1: // Downward sense single RA
                    if (ownAirplaneVsMps > maxDownSenseVsMps) {
                        // Corrective
                        if (maxDownSenseVsMps < 0) {
                            // Positive
                            primaryType = senseIsCrossing ? exports.TcasResolutionAdvisoryType.CrossingDescend : exports.TcasResolutionAdvisoryType.Descend;
                        }
                        else {
                            // Negative
                            primaryType = exports.TcasResolutionAdvisoryType.ReduceClimb;
                        }
                    }
                    else {
                        // Preventative
                        if (maxDownSenseVsMps < 0) {
                            // Positive
                            primaryType = senseIsCrossing ? exports.TcasResolutionAdvisoryType.CrossingMaintainDescend : exports.TcasResolutionAdvisoryType.MaintainDescend;
                        }
                        else {
                            // Negative
                            primaryType = TcasResolutionAdvisoryHostClass.getDoNotClimbType(maxDownSenseVsMps);
                        }
                    }
                    break;
            }
            this.setState(simTime, primaryType, secondaryType, true);
        }
        /**
         * Updates an existing positive resolution advisory. Positive resolution advisories include the CLIMB and DESCEND and
         * related types (CROSSING, MAINTAIN, INCREASE, REVERSAL).
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        updatePositive(simTime) {
            const sense = BitFlags.isAll(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.UpSense) ? 1 : -1;
            const isIncrease = BitFlags.isAll(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.Increase);
            // Check if we are inhibited, if so -> convert the current RA to its negative preventative counterpart.
            const isInhibited = sense === 1 ? !this.options.allowClimb(simTime) : !this.options.allowDescend(simTime);
            if (isInhibited) {
                this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.DoNotDescend0 : exports.TcasResolutionAdvisoryType.DoNotClimb0, null);
                return;
            }
            // Check if we are currently on track to achieve ALIM separation from all intruders. If we are, attempt to
            // weaken the RA. If we are not, attempt to strengthen the RA or trigger a sense reversal.
            const currentTargetVsMps = sense === 1 ? this._minVerticalSpeed.asUnit(UnitType.MPS) : this._maxVerticalSpeed.asUnit(UnitType.MPS);
            const requiredVsMps = sense === 1 ? this.getUpSenseRequiredMinVs() : this.getDownSenseRequiredMaxVs();
            const willAchieveAlim = (currentTargetVsMps - requiredVsMps) * sense >= 0;
            if (willAchieveAlim) {
                // If weakening to a REDUCE CLIMB/DESCENT RA would still provide ALIM separation, do so.
                if (requiredVsMps * sense <= 0) {
                    this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.ReduceDescent : exports.TcasResolutionAdvisoryType.ReduceClimb, null);
                }
            }
            else {
                // Check if strengthening to an INCREASE CLIMB/DESCENT RA would provide ALIM separation
                const isStrengthenInhibited = sense === 1 ? !this.options.allowIncreaseClimb(simTime) : !this.options.allowIncreaseDescent(simTime);
                let strengthen = !isIncrease && !isStrengthenInhibited && (TcasResolutionAdvisoryHostClass.INC_CLIMB_DESC_VS_MPS * sense - requiredVsMps) * sense >= 0;
                let reverseSense = false;
                if (!strengthen && this.senseReversalCount < this.intruderArray.length) {
                    // Check if a sense reversal would provide ALIM separation
                    const reversalRequiredVsMps = sense === 1 ? this.getDownSenseRequiredMaxVs() : this.getUpSenseRequiredMinVs();
                    reverseSense = (TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS * sense + reversalRequiredVsMps) * sense >= 0;
                }
                strengthen = !isIncrease && !isStrengthenInhibited && !reverseSense;
                if (strengthen) {
                    this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.IncreaseClimb : exports.TcasResolutionAdvisoryType.IncreaseDescend, null);
                }
                else if (reverseSense) {
                    this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.ReversalDescend : exports.TcasResolutionAdvisoryType.ReversalClimb, null);
                }
            }
        }
        /**
         * Updates an existing negative resolution advisory. Negative resolution advisories include the REDUCE CLIMB/DESCENT
         * and DO NOT CLIMB/DESCEND types.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplaneVsMps The current vertical speed of the own airplane, in meters per second.
         */
        updateNegative(simTime, ownAirplaneVsMps) {
            const sense = BitFlags.isAll(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.UpSense) ? 1 : -1;
            // Check if we are currently on track to achieve ALIM separation from all intruders. If we are, we do nothing since
            // negative RAs cannot be weakened. If we are not, attempt to strengthen the RA or trigger a sense reversal.
            const currentTargetVsMps = sense === 1 ? this._minVerticalSpeed.asUnit(UnitType.MPS) : this._maxVerticalSpeed.asUnit(UnitType.MPS);
            const requiredVsMps = sense === 1 ? this.getUpSenseRequiredMinVs() : this.getDownSenseRequiredMaxVs();
            const willAchieveAlim = (currentTargetVsMps - requiredVsMps) * sense >= 0;
            if (willAchieveAlim) {
                return;
            }
            const requirePositive = requiredVsMps * sense > 0;
            if (requirePositive) {
                // We need to strengthen to a positive RA in order to achieve ALIM separation from all intruders.
                const isStrengthenInhibited = sense === 1 ? !this.options.allowClimb(simTime) : !this.options.allowDescend(simTime);
                let strengthen = !isStrengthenInhibited && (TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS * sense - requiredVsMps) * sense >= 0;
                let reverseSense = false;
                if (!strengthen
                    && this.senseReversalCount < this.intruderArray.length
                    && (sense === 1 ? this.options.allowDescend(simTime) : this.options.allowClimb(simTime))) {
                    // Check if a sense reversal would provide ALIM separation
                    const reversalRequiredVsMps = sense === 1 ? this.getDownSenseRequiredMaxVs() : this.getUpSenseRequiredMinVs();
                    reverseSense = (TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS * sense + reversalRequiredVsMps) * sense >= 0;
                }
                strengthen = !isStrengthenInhibited && !reverseSense;
                if (strengthen) {
                    this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.Climb : exports.TcasResolutionAdvisoryType.Descend, null);
                    return;
                }
                else if (reverseSense) {
                    this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.ReversalDescend : exports.TcasResolutionAdvisoryType.ReversalClimb, null);
                    return;
                }
                // If we can't strengthen to a positive RA or issue a sense reversal, fall back to issuing the most restrictive
                // negative RA we can.
            }
            if ((ownAirplaneVsMps - requiredVsMps) * sense < 0) {
                // We need to issue a corrective negative RA.
                this.setState(simTime, sense === 1 ? exports.TcasResolutionAdvisoryType.ReduceDescent : exports.TcasResolutionAdvisoryType.ReduceClimb, null);
            }
            else {
                // We can strengthen to a more restrictive preventative negative RA.
                this.setState(simTime, sense === 1
                    ? TcasResolutionAdvisoryHostClass.getDoNotDescendType(requiredVsMps)
                    : TcasResolutionAdvisoryHostClass.getDoNotClimbType(requiredVsMps), null);
            }
        }
        /**
         * Updates an existing composite resolution advisory. Composite resolution advisories consist of a (corrective or
         * preventative) negative resolution advisory combined with a negative preventative resolution advisory of the
         * opposite sense.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param ownAirplaneVsMps The current vertical speed of the own airplane, in meters per second.
         * @throws Error if a composite resolution advisory is not active.
         */
        updateComposite(simTime, ownAirplaneVsMps) {
            if (this._secondaryType === null) {
                throw new Error('TcasResolutionAdvisoryClass: attempted to update a composite RA when one was not active');
            }
            const currentMinVsMps = this._minVerticalSpeed.asUnit(UnitType.MPS);
            const currentMaxVsMps = this._maxVerticalSpeed.asUnit(UnitType.MPS);
            const requiredMinVsMps = this.getCompositeRequiredMinVs();
            const requiredMaxVsMps = this.getCompositeRequiredMaxVs();
            const primarySense = BitFlags.isAll(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.UpSense) ? 1 : -1;
            let primaryCurrentVsMps, primaryRequiredVsMps, secondaryCurrentVsMps, secondaryRequiredVsMps;
            if (primarySense === 1) {
                primaryCurrentVsMps = currentMinVsMps;
                primaryRequiredVsMps = requiredMinVsMps;
                secondaryCurrentVsMps = currentMaxVsMps;
                secondaryRequiredVsMps = requiredMaxVsMps;
            }
            else {
                primaryCurrentVsMps = currentMaxVsMps;
                primaryRequiredVsMps = requiredMaxVsMps;
                secondaryCurrentVsMps = currentMinVsMps;
                secondaryRequiredVsMps = requiredMinVsMps;
            }
            const willPrimaryAchieveAlim = (primaryCurrentVsMps - primaryRequiredVsMps) * primarySense >= 0;
            const willSecondaryAchieveAlim = (secondaryCurrentVsMps - secondaryRequiredVsMps) * -primarySense >= 0;
            const requirePositive = !willPrimaryAchieveAlim && primaryRequiredVsMps * primarySense > 0
                || !willSecondaryAchieveAlim && secondaryRequiredVsMps * -primarySense > 0;
            if (requirePositive) {
                // We need to strengthen to a positive RA in order to achieve ALIM separation from all intruders.
                const isStrengthenInhibited = primarySense === 1 ? !this.options.allowClimb(simTime) : !this.options.allowDescend(simTime);
                const positiveRequiredVsMps = primarySense === 1 ? this.getUpSenseRequiredMinVs() : this.getDownSenseRequiredMaxVs();
                let strengthen = !isStrengthenInhibited && (TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS * primarySense - positiveRequiredVsMps) * primarySense >= 0;
                let reverseSense = false;
                if (!strengthen
                    && this.senseReversalCount < this.intruderArray.length
                    && (primarySense === 1 ? this.options.allowDescend(simTime) : this.options.allowClimb(simTime))) {
                    // Check if a sense reversal would provide ALIM separation
                    const reversalRequiredVsMps = primarySense === 1 ? this.getDownSenseRequiredMaxVs() : this.getUpSenseRequiredMinVs();
                    reverseSense = (TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS * primarySense + reversalRequiredVsMps) * primarySense >= 0;
                }
                strengthen = !isStrengthenInhibited && !reverseSense;
                if (strengthen) {
                    this.setState(simTime, primarySense === 1 ? exports.TcasResolutionAdvisoryType.Climb : exports.TcasResolutionAdvisoryType.Descend, null);
                    return;
                }
                else if (reverseSense) {
                    this.setState(simTime, primarySense === 1 ? exports.TcasResolutionAdvisoryType.ReversalDescend : exports.TcasResolutionAdvisoryType.ReversalClimb, null);
                    return;
                }
                // If we can't strengthen to a positive RA or issue a sense reversal, fall back to issuing the most restrictive
                // pair of negative RAs we can.
            }
            let primaryType;
            if ((ownAirplaneVsMps - primaryRequiredVsMps) * primarySense < 0) {
                // We need to issue a corrective negative RA.
                primaryType = primarySense === 1 ? exports.TcasResolutionAdvisoryType.ReduceDescent : exports.TcasResolutionAdvisoryType.ReduceClimb;
            }
            else {
                // We can strengthen to a more restrictive preventative negative RA.
                primaryType = primarySense === 1
                    ? TcasResolutionAdvisoryHostClass.getDoNotDescendType(primaryRequiredVsMps)
                    : TcasResolutionAdvisoryHostClass.getDoNotClimbType(primaryRequiredVsMps);
            }
            const secondaryType = primarySense === 1
                ? TcasResolutionAdvisoryHostClass.getDoNotClimbType(secondaryRequiredVsMps)
                : TcasResolutionAdvisoryHostClass.getDoNotDescendType(secondaryRequiredVsMps);
            this.setState(simTime, primaryType, secondaryType);
        }
        /**
         * Sets the state of this host's current resolution advisory. If the specified state is equal to the current state
         * of the resolution advisory, this method does nothing. If the state is successfully set, the `tcas_ra_issued`
         * event will be published if `isInitial` is `true`, or the `tcas_ra_updated` event if `isInitial` is `false`.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         * @param primaryType The primary type of the resolution advisory.
         * @param secondaryType The secondary type of the resolution advisory.
         * @param isInitial Whether the state to set is for an initial resolution advisory.
         */
        setState(simTime, primaryType, secondaryType, isInitial = false) {
            if (primaryType === this._primaryType && secondaryType === this._secondaryType) {
                return;
            }
            let minSpeed, maxSpeed;
            const primaryTypeDef = TcasResolutionAdvisoryHostClass.TYPE_DEFS[primaryType];
            this._primaryType = primaryType;
            this._primaryFlags = primaryTypeDef.flags;
            this._secondaryType = secondaryType;
            if (secondaryType !== null) {
                const secondaryTypeDef = TcasResolutionAdvisoryHostClass.TYPE_DEFS[secondaryType];
                this._secondaryFlags = secondaryTypeDef.flags;
                minSpeed = Math.max(primaryTypeDef.minVerticalSpeed, secondaryTypeDef.minVerticalSpeed);
                maxSpeed = Math.min(primaryTypeDef.maxVerticalSpeed, secondaryTypeDef.maxVerticalSpeed);
            }
            else {
                this._secondaryFlags = 0;
                minSpeed = primaryTypeDef.minVerticalSpeed;
                maxSpeed = primaryTypeDef.maxVerticalSpeed;
            }
            this._minVerticalSpeed.set(isFinite(minSpeed) ? minSpeed : NaN);
            this._maxVerticalSpeed.set(isFinite(maxSpeed) ? maxSpeed : NaN);
            this.isInitial = isInitial;
            this.lastStateChangeTime = simTime;
            this.stateChangeDelay = TcasResolutionAdvisoryHostClass.STATE_CHANGE_DELAY_BASE
                + (isInitial ? this.initialResponseTimeSeconds : this.subsequentResponseTimeSeconds) * 1000;
            if (BitFlags.isAll(this._primaryFlags, exports.TcasResolutionAdvisoryFlags.Reversal)) {
                this.senseReversalCount++;
            }
            if (isInitial) {
                this.publisher.pub('tcas_ra_issued', this, false, false);
            }
            else {
                this.publisher.pub('tcas_ra_updated', this, false, false);
            }
        }
        /**
         * Cancels this host's current resolution advisory. If there is no currently active resolution advisory, this
         * method does nothing. If the resolution advisory is successfully cancelled, the `tcas_ra_canceled` event will be
         * published.
         * @param simTime The current sim time, as a UNIX timestamp in milliseconds.
         */
        cancel(simTime) {
            if (this._primaryType === exports.TcasResolutionAdvisoryType.Clear && this._secondaryType === null) {
                return;
            }
            this.intruders.clear();
            this.intruderArray.length = 0;
            this._maxVerticalSpeed.set(NaN);
            this._minVerticalSpeed.set(NaN);
            this._primaryType = exports.TcasResolutionAdvisoryType.Clear;
            this._primaryFlags = 0;
            this._secondaryType = null;
            this._secondaryFlags = 0;
            this.lastStateChangeTime = simTime;
            this.stateChangeDelay = TcasResolutionAdvisoryHostClass.STATE_CHANGE_DELAY_BASE;
            this.isInitial = true;
            this.senseReversalCount = 0;
            this.publisher.pub('tcas_ra_canceled', undefined, false, false);
        }
        /**
         * Gets the required minimum vertical speed, in meters per second, for the own airplane to achieve ALIM separation
         * above all current RA intruders.
         * @returns The required minimum vertical speed, in meters per second, for the own airplane to achieve ALIM
         * separation above all current RA intruders.
         */
        getUpSenseRequiredMinVs() {
            return this.vsConstraints.reduce((min, constraint) => Math.max(min, constraint.above), -Infinity);
        }
        /**
         * Gets the required maximum vertical speed, in meters per second, for the own airplane to achieve ALIM separation
         * below all current RA intruders.
         * @returns The required maximum vertical speed, in meters per second, for the own airplane to achieve ALIM
         * separation below all current RA intruders.
         */
        getDownSenseRequiredMaxVs() {
            return this.vsConstraints.reduce((max, constraint) => Math.min(max, constraint.below), Infinity);
        }
        /**
         * Gets the required minimum vertical speed, in meters per second, for the own airplane to achieve ALIM separation
         * from all current RA intruders during a composite advisory.
         * @returns The required minimum vertical speed, in meters per second, for the own airplane to achieve ALIM
         * separation from all current RA intruders during a composite advisory.
         */
        getCompositeRequiredMinVs() {
            return this.vsConstraints.reduce((min, constraint) => constraint.above <= 0 ? Math.max(min, constraint.above) : min, -Infinity);
        }
        /**
         * Gets the required maximum vertical speed, in meters per second, for the own airplane to achieve ALIM separation
         * from all current RA intruders during a composite advisory.
         * @returns The required maximum vertical speed, in meters per second, for the own airplane to achieve ALIM
         * separation from all current RA intruders during a composite advisory.
         */
        getCompositeRequiredMaxVs() {
            return this.vsConstraints.reduce((max, constraint) => constraint.below >= 0 ? Math.min(max, constraint.below) : max, Infinity);
        }
        /**
         * Calculates the vertical speed required to achieve a desired altitude target at time of closest approach.
         * @param tcpa The time to closest approach from the present, in seconds.
         * @param currentAlt The current altitude of the own airplane, in meters.
         * @param vs The current vertical speed of the own airplane, in meters per second.
         * @param responseTime The response time of the own airplane, in seconds.
         * @param accel The acceleration of the own airplane, in meters per second squared.
         * @param targetAlt The target altitude of the own airplane at time of closest approach, in meters.
         * @returns The vertical speed, in meters per second, required to achieve a desired altitude target at time of
         * closest approach. A value of `NaN` indicates the altitude target cannot be reached with the specified parameters.
         */
        static calculateVSToTargetAlt(tcpa, currentAlt, vs, responseTime, accel, targetAlt) {
            const signedAccel = accel * Math.sign(targetAlt - (currentAlt + vs * tcpa));
            if (signedAccel === 0) {
                return vs;
            }
            const y0 = currentAlt + vs * responseTime;
            const tc = tcpa - responseTime;
            const a = signedAccel / 2;
            const b = -signedAccel * tc;
            const c = targetAlt - y0 - vs * tc;
            const discriminant = b * b - 4 * a * c;
            if (discriminant < 0) {
                return NaN;
            }
            const sqrtDiscr = Math.sqrt(discriminant);
            const t1 = (-b + sqrtDiscr) / (2 * a);
            const t2 = (-b - sqrtDiscr) / (2 * a);
            if (t1 <= tc && t1 >= 0) {
                return vs + signedAccel * t1;
            }
            if (t2 <= tc && t2 >= 0) {
                return vs + signedAccel * t2;
            }
            return NaN;
        }
        /**
         * Gets the least restrictive DO NOT DESCEND resolution advisory type appropriate to issue for a given minimum
         * vertical speed requirement.
         * @param minVsMps A minimum vertical speed requirement, in meters per second.
         * @returns The least restrictive DO NOT DESCEND resolution advisory type appropriate to issue for the specified
         * minimum vertical speed requirement.
         */
        static getDoNotDescendType(minVsMps) {
            if (minVsMps > -500) {
                return exports.TcasResolutionAdvisoryType.DoNotDescend0;
            }
            else if (minVsMps > -1000) {
                return exports.TcasResolutionAdvisoryType.DoNotDescend500;
            }
            else if (minVsMps > -1500) {
                return exports.TcasResolutionAdvisoryType.DoNotDescend1000;
            }
            else if (minVsMps > -2000) {
                return exports.TcasResolutionAdvisoryType.DoNotDescend1500;
            }
            else {
                return exports.TcasResolutionAdvisoryType.DoNotDescend2000;
            }
        }
        /**
         * Gets the least restrictive DO NOT CLIMB resolution advisory type appropriate to issue for a given maximum
         * vertical speed requirement.
         * @param maxVsMps A maximum vertical speed requirement, in meters per second.
         * @returns The least restrictive DO NOT CLIMB resolution advisory type appropriate to issue for the specified
         * maximum vertical speed requirement.
         */
        static getDoNotClimbType(maxVsMps) {
            if (maxVsMps < 500) {
                return exports.TcasResolutionAdvisoryType.DoNotClimb0;
            }
            else if (maxVsMps < 1000) {
                return exports.TcasResolutionAdvisoryType.DoNotClimb500;
            }
            else if (maxVsMps < 1500) {
                return exports.TcasResolutionAdvisoryType.DoNotClimb1000;
            }
            else if (maxVsMps < 2000) {
                return exports.TcasResolutionAdvisoryType.DoNotClimb1500;
            }
            else {
                return exports.TcasResolutionAdvisoryType.DoNotClimb2000;
            }
        }
    }
    /** The base amount of delay between state changes, in milliseconds, before predicted response time is taken into account. */
    TcasResolutionAdvisoryHostClass.STATE_CHANGE_DELAY_BASE = 5000;
    TcasResolutionAdvisoryHostClass.CLIMB_DESC_VS_MPS = UnitType.FPM.convertTo(1500, UnitType.MPS);
    TcasResolutionAdvisoryHostClass.INC_CLIMB_DESC_VS_MPS = UnitType.FPM.convertTo(2500, UnitType.MPS);
    TcasResolutionAdvisoryHostClass.INTRUDER_SORT_FUNC = (a, b) => {
        const tcpaComparison = a.tcaRA.tcpa.compare(b.tcaRA.tcpa);
        if (tcpaComparison < 0) {
            return -1;
        }
        else if (tcpaComparison > 0) {
            return 1;
        }
        else if (a.tcaRA.cpaNorm < b.tcaRA.cpaNorm) {
            return -1;
        }
        else if (a.tcaRA.cpaNorm > b.tcaRA.cpaNorm) {
            return 1;
        }
        else {
            return 0;
        }
    };
    TcasResolutionAdvisoryHostClass.TYPE_DEFS = {
        [exports.TcasResolutionAdvisoryType.Clear]: {
            flags: 0,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.Climb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: 1500,
            maxVerticalSpeed: 2000
        },
        [exports.TcasResolutionAdvisoryType.MaintainClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb,
            minVerticalSpeed: 1500,
            maxVerticalSpeed: 4400
        },
        [exports.TcasResolutionAdvisoryType.CrossingClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Crossing | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: 1500,
            maxVerticalSpeed: 2000
        },
        [exports.TcasResolutionAdvisoryType.CrossingMaintainClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Crossing,
            minVerticalSpeed: 1500,
            maxVerticalSpeed: 4400
        },
        [exports.TcasResolutionAdvisoryType.IncreaseClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Increase | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: 2500,
            maxVerticalSpeed: 3000
        },
        [exports.TcasResolutionAdvisoryType.ReversalClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.Climb | exports.TcasResolutionAdvisoryFlags.Reversal | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: 1500,
            maxVerticalSpeed: 2000
        },
        [exports.TcasResolutionAdvisoryType.ReduceDescent]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.ReduceDescent | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: 0,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.DoNotDescend0]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.DoNotDescend,
            minVerticalSpeed: 0,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.DoNotDescend500]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.DoNotDescend,
            minVerticalSpeed: -500,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.DoNotDescend1000]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.DoNotDescend,
            minVerticalSpeed: -1000,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.DoNotDescend1500]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.DoNotDescend,
            minVerticalSpeed: -1500,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.DoNotDescend2000]: {
            flags: exports.TcasResolutionAdvisoryFlags.UpSense | exports.TcasResolutionAdvisoryFlags.DoNotDescend,
            minVerticalSpeed: -2000,
            maxVerticalSpeed: Infinity
        },
        [exports.TcasResolutionAdvisoryType.Descend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: -2000,
            maxVerticalSpeed: -1500
        },
        [exports.TcasResolutionAdvisoryType.MaintainDescend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend,
            minVerticalSpeed: -4400,
            maxVerticalSpeed: -1500
        },
        [exports.TcasResolutionAdvisoryType.CrossingDescend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend | exports.TcasResolutionAdvisoryFlags.Crossing | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: -2000,
            maxVerticalSpeed: -1500
        },
        [exports.TcasResolutionAdvisoryType.CrossingMaintainDescend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend | exports.TcasResolutionAdvisoryFlags.Crossing,
            minVerticalSpeed: -4400,
            maxVerticalSpeed: -1500
        },
        [exports.TcasResolutionAdvisoryType.IncreaseDescend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend | exports.TcasResolutionAdvisoryFlags.Increase | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: -3000,
            maxVerticalSpeed: -2500
        },
        [exports.TcasResolutionAdvisoryType.ReversalDescend]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.Descend | exports.TcasResolutionAdvisoryFlags.Reversal | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: -2000,
            maxVerticalSpeed: -1500
        },
        [exports.TcasResolutionAdvisoryType.ReduceClimb]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.ReduceClimb | exports.TcasResolutionAdvisoryFlags.Corrective,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 0
        },
        [exports.TcasResolutionAdvisoryType.DoNotClimb0]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.DoNotClimb,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 0
        },
        [exports.TcasResolutionAdvisoryType.DoNotClimb500]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.DoNotClimb,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 500
        },
        [exports.TcasResolutionAdvisoryType.DoNotClimb1000]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.DoNotClimb,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 1000
        },
        [exports.TcasResolutionAdvisoryType.DoNotClimb1500]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.DoNotClimb,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 1500
        },
        [exports.TcasResolutionAdvisoryType.DoNotClimb2000]: {
            flags: exports.TcasResolutionAdvisoryFlags.DownSense | exports.TcasResolutionAdvisoryFlags.DoNotClimb,
            minVerticalSpeed: -Infinity,
            maxVerticalSpeed: 2000
        },
    };
    TcasResolutionAdvisoryHostClass.vec3Cache = [new Float64Array(3)];

    /**
     * A default implementation of {@link TcasAdvisoryDataProvider}.
     */
    class DefaultTcasAdvisoryDataProvider {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param tcas The TCAS instance.
         */
        constructor(bus, tcas) {
            this.bus = bus;
            this.tcas = tcas;
            this._paIntruders = SetSubject.create();
            this.paIntruders = this._paIntruders;
            this._taIntruders = SetSubject.create();
            this.taIntruders = this._taIntruders;
            this._raIntruders = SetSubject.create();
            this.raIntruders = this._raIntruders;
            this.isInit = false;
            this.isAlive = true;
            this.isPaused = false;
        }
        /**
         * Initializes this data provider. Once initialized, this data provider will continuously update its data until
         * paused or destroyed.
         * @param paused Whether to initialize this data provider as paused. If `true`, this data provider will provide an
         * initial set of data but will not update the provided data until it is resumed. Defaults to `false`.
         * @throws Error if this data provider is dead.
         */
        init(paused = false) {
            if (!this.isAlive) {
                throw new Error('DefaultTcasAdvisoryDataProvider: cannot initialize a dead provider');
            }
            if (this.isInit) {
                return;
            }
            this.isInit = true;
            this.isPaused = paused;
            this.updateIntruders();
            const sub = this.bus.getSubscriber();
            this.intruderAlertLevelSub = sub.on('tcas_intruder_alert_changed').handle(intruder => {
                const alertLevel = intruder.alertLevel.get();
                this._paIntruders.toggle(intruder, alertLevel === exports.TcasAlertLevel.ProximityAdvisory);
                this._taIntruders.toggle(intruder, alertLevel === exports.TcasAlertLevel.TrafficAdvisory);
                this._raIntruders.toggle(intruder, alertLevel === exports.TcasAlertLevel.ResolutionAdvisory);
            });
            this.intruderRemovedSub = sub.on('tcas_intruder_removed').handle(intruder => {
                this._paIntruders.delete(intruder);
                this._taIntruders.delete(intruder);
                this._raIntruders.delete(intruder);
            });
            if (paused) {
                this.pause();
            }
        }
        /**
         * Resumes this data provider. Once resumed, this data provider will continuously update its data until paused or
         * destroyed.
         * @throws Error if this data provider is dead.
         */
        resume() {
            var _a, _b;
            if (!this.isAlive) {
                throw new Error('DefaultTcasAdvisoryDataProvider: cannot resume a dead provider');
            }
            if (!this.isPaused) {
                return;
            }
            this.isPaused = false;
            this.updateIntruders();
            (_a = this.intruderAlertLevelSub) === null || _a === void 0 ? void 0 : _a.resume();
            (_b = this.intruderRemovedSub) === null || _b === void 0 ? void 0 : _b.resume();
        }
        /**
         * Pauses this data provider. Once paused, this data provider will not update its data until it is resumed.
         * @throws Error if this data provider is dead.
         */
        pause() {
            var _a, _b;
            if (!this.isAlive) {
                throw new Error('DefaultTcasAdvisoryDataProvider: cannot pause a dead provider');
            }
            if (this.isPaused) {
                return;
            }
            this.isPaused = true;
            (_a = this.intruderAlertLevelSub) === null || _a === void 0 ? void 0 : _a.pause();
            (_b = this.intruderRemovedSub) === null || _b === void 0 ? void 0 : _b.pause();
        }
        /**
         * Updates this provider's sets of intruders associated with active advisories.
         */
        updateIntruders() {
            const paIntruders = new Set();
            const taIntruders = new Set();
            const raIntruders = new Set();
            // Collect the sets of intruders associated with each type of advisory.
            const intruders = this.tcas.getIntruders();
            for (let i = 0; i < intruders.length; i++) {
                const intruder = intruders[i];
                switch (intruder.alertLevel.get()) {
                    case exports.TcasAlertLevel.ProximityAdvisory:
                        paIntruders.add(intruder);
                        break;
                    case exports.TcasAlertLevel.TrafficAdvisory:
                        taIntruders.add(intruder);
                        break;
                    case exports.TcasAlertLevel.ResolutionAdvisory:
                        raIntruders.add(intruder);
                        break;
                }
            }
            // From each set, remove all intruders no longer associated with that type of advisory.
            for (const intruder of this._paIntruders.get()) {
                if (!paIntruders.delete(intruder)) {
                    this._paIntruders.delete(intruder);
                }
            }
            for (const intruder of this._taIntruders.get()) {
                if (!taIntruders.delete(intruder)) {
                    this._taIntruders.delete(intruder);
                }
            }
            for (const intruder of this._raIntruders.get()) {
                if (!raIntruders.delete(intruder)) {
                    this._raIntruders.delete(intruder);
                }
            }
            // Add the new intruders associated with each type of advisory to each set.
            for (const intruder of paIntruders) {
                this._paIntruders.add(intruder);
            }
            for (const intruder of taIntruders) {
                this._taIntruders.add(intruder);
            }
            for (const intruder of raIntruders) {
                this._raIntruders.add(intruder);
            }
        }
        /**
         * Destroys this data provider. Once destroyed, this data provider will no longer update its provided data, and can
         * no longer be paused or resumed.
         */
        destroy() {
            var _a, _b;
            this.isAlive = false;
            (_a = this.intruderAlertLevelSub) === null || _a === void 0 ? void 0 : _a.destroy();
            (_b = this.intruderRemovedSub) === null || _b === void 0 ? void 0 : _b.destroy();
        }
    }

    /**
     * Standard TCAS-II sensitivity parameters.
     */
    class TcasIISensitivityParameters {
        /**
         * Selects a sensitivity level for a specified environment.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         * @returns The sensitivity level for the specified environment.
         */
        selectLevel(altitude, radarAltitude) {
            const altFeet = altitude.asUnit(UnitType.FOOT);
            const radarAltFeet = radarAltitude.asUnit(UnitType.FOOT);
            let level;
            if (radarAltFeet > 2350) {
                if (altFeet > 42000) {
                    level = 6;
                }
                else if (altFeet > 20000) {
                    level = 5;
                }
                else if (altFeet > 10000) {
                    level = 4;
                }
                else if (altFeet > 5000) {
                    level = 3;
                }
                else {
                    level = 2;
                }
            }
            else if (radarAltFeet > 1000) {
                level = 1;
            }
            else {
                level = 0;
            }
            return level;
        }
        /**
         * Selects Proximity Advisory sensitivity parameters.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         * @returns Proximity Advisory sensitivity parameters.
         */
        selectPA(
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        altitude, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        radarAltitude) {
            return TcasIISensitivityParameters.PA;
        }
        /**
         * Selects Traffic Advisory sensitivity parameters for a specified environment.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         * @returns Traffic Advisory sensitivity parameters for the specified environment.
         */
        selectTA(altitude, radarAltitude) {
            return TcasIISensitivityParameters.TA_LEVELS[this.selectLevel(altitude, radarAltitude)];
        }
        /**
         * Selects Resolution Advisory sensitivity parameters for a specified environment.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         * @returns Resolution Advisory sensitivity parameters for the specified environment.
         */
        selectRA(altitude, radarAltitude) {
            return TcasIISensitivityParameters.RA_LEVELS[this.selectLevel(altitude, radarAltitude)];
        }
        /**
         * Selects a Resolution Advisory ALIM for a specified environment.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         * @returns A Resolution Advisory ALIM for the specified environment.
         */
        selectRAAlim(altitude, radarAltitude) {
            return TcasIISensitivityParameters.RA_LEVELS[this.selectLevel(altitude, radarAltitude)].alim;
        }
        /**
         * Gets Proximity Advisory sensitivity parameters for a given sensitivity level.
         * @param level A sensitivity level.
         * @returns Proximity Advisory sensitivity parameters for the given sensitivity level.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        getPA(level) {
            return TcasIISensitivityParameters.PA;
        }
        /**
         * Gets Traffic Advisory sensitivity parameters for a given sensitivity level.
         * @param level A sensitivity level.
         * @returns Traffic Advisory sensitivity parameters for the given sensitivity level.
         */
        getTA(level) {
            return TcasIISensitivityParameters.TA_LEVELS[MathUtils.clamp(level, 0, TcasIISensitivityParameters.TA_LEVELS.length - 1)];
        }
        /**
         * Gets Resolution Advisory sensitivity parameters for a given sensitivity level.
         * @param level A sensitivity level.
         * @returns Resolution Advisory sensitivity parameters for the given sensitivity level.
         */
        getRA(level) {
            return TcasIISensitivityParameters.RA_LEVELS[MathUtils.clamp(level, 0, TcasIISensitivityParameters.RA_LEVELS.length - 1)];
        }
        /**
         * Gets a Resolution Advisory ALIM for a given sensitivity level.
         * @param level A sensitivity level.
         * @returns A Resolution Advisory ALIM for the given sensitivity level.
         */
        getRAAlim(level) {
            return TcasIISensitivityParameters.RA_LEVELS[MathUtils.clamp(level, 0, TcasIISensitivityParameters.RA_LEVELS.length - 1)].alim;
        }
    }
    TcasIISensitivityParameters.PA = {
        protectedRadius: UnitType.NMILE.createNumber(6),
        protectedHeight: UnitType.FOOT.createNumber(1200)
    };
    TcasIISensitivityParameters.TA_LEVELS = [
        {
            tau: UnitType.SECOND.createNumber(20),
            protectedRadius: UnitType.NMILE.createNumber(0.3),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(25),
            protectedRadius: UnitType.NMILE.createNumber(0.33),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(30),
            protectedRadius: UnitType.NMILE.createNumber(0.48),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(40),
            protectedRadius: UnitType.NMILE.createNumber(0.75),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(45),
            protectedRadius: UnitType.NMILE.createNumber(1),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(48),
            protectedRadius: UnitType.NMILE.createNumber(1.3),
            protectedHeight: UnitType.FOOT.createNumber(850)
        },
        {
            tau: UnitType.SECOND.createNumber(48),
            protectedRadius: UnitType.NMILE.createNumber(1.3),
            protectedHeight: UnitType.FOOT.createNumber(1200)
        }
    ];
    TcasIISensitivityParameters.RA_LEVELS = [
        {
            tau: UnitType.SECOND.createNumber(15),
            protectedRadius: UnitType.NMILE.createNumber(0.2),
            protectedHeight: UnitType.FOOT.createNumber(600),
            alim: UnitType.FOOT.createNumber(300),
            hmd: UnitType.NMILE.createNumber(0.4)
        },
        {
            tau: UnitType.SECOND.createNumber(15),
            protectedRadius: UnitType.NMILE.createNumber(0.2),
            protectedHeight: UnitType.FOOT.createNumber(600),
            alim: UnitType.FOOT.createNumber(300),
            hmd: UnitType.NMILE.createNumber(0.4)
        },
        {
            tau: UnitType.SECOND.createNumber(20),
            protectedRadius: UnitType.NMILE.createNumber(0.35),
            protectedHeight: UnitType.FOOT.createNumber(600),
            alim: UnitType.FOOT.createNumber(300),
            hmd: UnitType.NMILE.createNumber(0.57)
        },
        {
            tau: UnitType.SECOND.createNumber(25),
            protectedRadius: UnitType.NMILE.createNumber(0.55),
            protectedHeight: UnitType.FOOT.createNumber(600),
            alim: UnitType.FOOT.createNumber(350),
            hmd: UnitType.NMILE.createNumber(0.74)
        },
        {
            tau: UnitType.SECOND.createNumber(30),
            protectedRadius: UnitType.NMILE.createNumber(0.8),
            protectedHeight: UnitType.FOOT.createNumber(600),
            alim: UnitType.FOOT.createNumber(400),
            hmd: UnitType.NMILE.createNumber(0.82)
        },
        {
            tau: UnitType.SECOND.createNumber(35),
            protectedRadius: UnitType.NMILE.createNumber(1.1),
            protectedHeight: UnitType.FOOT.createNumber(700),
            alim: UnitType.FOOT.createNumber(600),
            hmd: UnitType.NMILE.createNumber(0.98)
        },
        {
            tau: UnitType.SECOND.createNumber(35),
            protectedRadius: UnitType.NMILE.createNumber(1.1),
            protectedHeight: UnitType.FOOT.createNumber(800),
            alim: UnitType.FOOT.createNumber(700),
            hmd: UnitType.NMILE.createNumber(0.98)
        }
    ];
    /**
     * An implementation of {@link TCASSensitivity} which provides sensitivity parameters as defined in the official
     * TCAS II specification.
     */
    class TcasIISensitivity {
        constructor() {
            this.sensitivity = new TcasIISensitivityParameters();
            this.level = 0;
            this.params = {
                parametersPA: this.sensitivity.getPA(0),
                parametersTA: this.sensitivity.getTA(0),
                parametersRA: this.sensitivity.getRA(0)
            };
        }
        /** @inheritdoc */
        selectParameters() {
            return this.params;
        }
        /** @inheritdoc */
        selectRAAlim() {
            return this.sensitivity.getRAAlim(this.level);
        }
        /**
         * Updates sensitivity level based on the current environment.
         * @param altitude The indicated altitude of the own airplane.
         * @param radarAltitude The radar altitude of the own airplane.
         */
        updateLevel(altitude, radarAltitude) {
            this.level = this.sensitivity.selectLevel(altitude, radarAltitude);
            this.params.parametersPA = this.sensitivity.getPA(this.level);
            this.params.parametersTA = this.sensitivity.getTA(this.level);
            this.params.parametersRA = this.sensitivity.getRA(this.level);
        }
    }

    /**
     * Traffic alert level modes.
     */
    exports.MapTrafficAlertLevelVisibility = void 0;
    (function (MapTrafficAlertLevelVisibility) {
        MapTrafficAlertLevelVisibility[MapTrafficAlertLevelVisibility["Other"] = 1] = "Other";
        MapTrafficAlertLevelVisibility[MapTrafficAlertLevelVisibility["ProximityAdvisory"] = 2] = "ProximityAdvisory";
        MapTrafficAlertLevelVisibility[MapTrafficAlertLevelVisibility["TrafficAdvisory"] = 4] = "TrafficAdvisory";
        MapTrafficAlertLevelVisibility[MapTrafficAlertLevelVisibility["ResolutionAdvisory"] = 8] = "ResolutionAdvisory";
        MapTrafficAlertLevelVisibility[MapTrafficAlertLevelVisibility["All"] = 15] = "All";
    })(exports.MapTrafficAlertLevelVisibility || (exports.MapTrafficAlertLevelVisibility = {}));
    /**
     * A module describing the display of traffic.
     */
    class MapTrafficModule {
        /**
         * Creates an instance of a MapTrafficModule.
         * @param tcas This module's associated TCAS.
         */
        constructor(tcas) {
            this.tcas = tcas;
            /** Whether to show traffic information. */
            this.show = Subject.create(true);
            /** The TCAS operating mode. */
            this.operatingMode = Subject.create(exports.TcasOperatingMode.Standby);
            /**
             * The distance from the own airplane beyond which intruders are considered off-scale. If the value is `NaN`,
             * intruders are never considered off-scale.
             */
            this.offScaleRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(NaN));
            /** Alert level visibility flags. */
            this.alertLevelVisibility = Subject.create(exports.MapTrafficAlertLevelVisibility.All);
            /** The difference in altitude above the own airplane above which intruders will not be displayed. */
            this.altitudeRestrictionAbove = NumberUnitSubject.createFromNumberUnit(UnitType.FOOT.createNumber(9900));
            /** The difference in altitude below the own airplane below which intruders will not be displayed. */
            this.altitudeRestrictionBelow = NumberUnitSubject.createFromNumberUnit(UnitType.FOOT.createNumber(9900));
            /** Whether displayed intruder altitude is relative. */
            this.isAltitudeRelative = Subject.create(true);
            this.tcas.getEventSubscriber().on('tcas_operating_mode').whenChanged().handle(mode => {
                this.operatingMode.set(mode);
            });
        }
    }

    /**
     * A map layer which displays traffic intruders.
     */
    class MapSystemTrafficLayer extends MapLayer {
        constructor() {
            var _a;
            super(...arguments);
            this.iconLayerRef = exports.FSComponent.createRef();
            this.trafficModule = this.props.model.getModule(MapSystemKeys.Traffic);
            this.intruderIcons = {
                [exports.TcasAlertLevel.None]: new Map(),
                [exports.TcasAlertLevel.ProximityAdvisory]: new Map(),
                [exports.TcasAlertLevel.TrafficAdvisory]: new Map(),
                [exports.TcasAlertLevel.ResolutionAdvisory]: new Map()
            };
            this.needHandleOffscaleOob = this.props.offScaleIntruders !== undefined || this.props.oobIntruders !== undefined;
            this.oobOffset = (_a = this.props.oobOffset) !== null && _a !== void 0 ? _a : Subject.create(VecNMath.create(4));
            this.oobBounds = VecNSubject.createFromVector(VecNMath.create(4));
            this.isInit = false;
        }
        /** @inheritdoc */
        onVisibilityChanged(isVisible) {
            var _a, _b;
            if (!isVisible) {
                if (this.isInit) {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    this.iconLayerRef.instance.display.clear();
                }
                (_a = this.props.offScaleIntruders) === null || _a === void 0 ? void 0 : _a.clear();
                (_b = this.props.oobIntruders) === null || _b === void 0 ? void 0 : _b.clear();
            }
        }
        /** @inheritdoc */
        onAttached() {
            this.iconLayerRef.instance.onAttached();
            this.oobOffset.sub(this.updateOobBounds.bind(this), true);
            this.trafficModule.operatingMode.sub(this.updateVisibility.bind(this));
            this.trafficModule.show.sub(this.updateVisibility.bind(this), true);
            this.initCanvasStyles();
            this.initIntruders();
            this.initTCASHandlers();
            this.isInit = true;
        }
        /**
         * Initializes canvas styles.
         */
        initCanvasStyles() {
            this.props.initCanvasStyles && this.props.initCanvasStyles(this.iconLayerRef.instance.display.context);
        }
        /**
         * Initializes all currently existing TCAS intruders.
         */
        initIntruders() {
            const intruders = this.trafficModule.tcas.getIntruders();
            const len = intruders.length;
            for (let i = 0; i < len; i++) {
                this.onIntruderAdded(intruders[i]);
            }
        }
        /**
         * Initializes handlers to respond to TCAS events.
         */
        initTCASHandlers() {
            const tcasSub = this.props.context.bus.getSubscriber();
            tcasSub.on('tcas_intruder_added').handle(this.onIntruderAdded.bind(this));
            tcasSub.on('tcas_intruder_removed').handle(this.onIntruderRemoved.bind(this));
            tcasSub.on('tcas_intruder_alert_changed').handle(this.onIntruderAlertLevelChanged.bind(this));
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.iconLayerRef.instance.onMapProjectionChanged(mapProjection, changeFlags);
            if (BitFlags.isAll(changeFlags, exports.MapProjectionChangeType.ProjectedSize)) {
                this.initCanvasStyles();
                this.updateOobBounds();
            }
        }
        /**
         * Updates the boundaries of the intruder out-of-bounds area.
         */
        updateOobBounds() {
            const projectedSize = this.props.mapProjection.getProjectedSize();
            const oobOffset = this.oobOffset.get();
            this.oobBounds.set(oobOffset[0], oobOffset[1], projectedSize[0] - oobOffset[2], projectedSize[1] - oobOffset[3]);
        }
        /** @inheritdoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onUpdated(time, elapsed) {
            if (!this.isVisible()) {
                return;
            }
            this.redrawIntruders();
        }
        /**
         * Redraws all tracked intruders.
         */
        redrawIntruders() {
            const alertLevelVisFlags = this.trafficModule.alertLevelVisibility.get();
            const offScaleRange = this.trafficModule.offScaleRange.get();
            const oobBounds = this.oobBounds.get();
            const iconDisplay = this.iconLayerRef.instance.display;
            iconDisplay.clear();
            for (let i = 0; i < MapSystemTrafficLayer.DRAW_GROUPS.length; i++) {
                const group = MapSystemTrafficLayer.DRAW_GROUPS[i];
                if (BitFlags.isAll(alertLevelVisFlags, group.alertLevelVisFlag)) {
                    this.intruderIcons[group.alertLevel].forEach(icon => {
                        var _a, _b, _c, _d, _e, _f;
                        icon.draw(this.props.mapProjection, iconDisplay.context, offScaleRange);
                        if (this.needHandleOffscaleOob) {
                            if (icon.isOffScale) {
                                (_a = this.props.oobIntruders) === null || _a === void 0 ? void 0 : _a.delete(icon.intruder);
                                (_b = this.props.offScaleIntruders) === null || _b === void 0 ? void 0 : _b.add(icon.intruder);
                            }
                            else if (!this.props.mapProjection.isInProjectedBounds(icon.projectedPos, oobBounds)) {
                                (_c = this.props.offScaleIntruders) === null || _c === void 0 ? void 0 : _c.delete(icon.intruder);
                                (_d = this.props.oobIntruders) === null || _d === void 0 ? void 0 : _d.add(icon.intruder);
                            }
                            else {
                                (_e = this.props.offScaleIntruders) === null || _e === void 0 ? void 0 : _e.delete(icon.intruder);
                                (_f = this.props.oobIntruders) === null || _f === void 0 ? void 0 : _f.delete(icon.intruder);
                            }
                        }
                    });
                }
                else if (this.needHandleOffscaleOob) {
                    this.intruderIcons[group.alertLevel].forEach(icon => {
                        var _a, _b;
                        (_a = this.props.offScaleIntruders) === null || _a === void 0 ? void 0 : _a.delete(icon.intruder);
                        (_b = this.props.oobIntruders) === null || _b === void 0 ? void 0 : _b.delete(icon.intruder);
                    });
                }
            }
        }
        /**
         * Updates this layer's visibility.
         */
        updateVisibility() {
            const operatingMode = this.trafficModule.tcas.getOperatingMode();
            this.setVisible(this.trafficModule.show.get()
                && (operatingMode === exports.TcasOperatingMode.TAOnly
                    || operatingMode === exports.TcasOperatingMode.TA_RA
                    || operatingMode === exports.TcasOperatingMode.Test));
        }
        /**
         * A callback which is called when a TCAS intruder is added.
         * @param intruder The new intruder.
         */
        onIntruderAdded(intruder) {
            const icon = this.props.iconFactory(intruder, this.props.context);
            this.intruderIcons[intruder.alertLevel.get()].set(intruder, icon);
        }
        /**
         * A callback which is called when a TCAS intruder is removed.
         * @param intruder The removed intruder.
         */
        onIntruderRemoved(intruder) {
            var _a, _b;
            (_a = this.props.offScaleIntruders) === null || _a === void 0 ? void 0 : _a.delete(intruder);
            (_b = this.props.oobIntruders) === null || _b === void 0 ? void 0 : _b.delete(intruder);
            this.intruderIcons[intruder.alertLevel.get()].delete(intruder);
        }
        /**
         * A callback which is called when the alert level of a TCAS intruder is changed.
         * @param intruder The intruder.
         */
        onIntruderAlertLevelChanged(intruder) {
            let oldAlertLevel;
            let view = this.intruderIcons[oldAlertLevel = exports.TcasAlertLevel.None].get(intruder);
            view !== null && view !== void 0 ? view : (view = this.intruderIcons[oldAlertLevel = exports.TcasAlertLevel.ProximityAdvisory].get(intruder));
            view !== null && view !== void 0 ? view : (view = this.intruderIcons[oldAlertLevel = exports.TcasAlertLevel.TrafficAdvisory].get(intruder));
            view !== null && view !== void 0 ? view : (view = this.intruderIcons[oldAlertLevel = exports.TcasAlertLevel.ResolutionAdvisory].get(intruder));
            if (view) {
                this.intruderIcons[oldAlertLevel].delete(intruder);
                this.intruderIcons[intruder.alertLevel.get()].set(intruder, view);
            }
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent(MapSyncedCanvasLayer, { ref: this.iconLayerRef, model: this.props.model, mapProjection: this.props.mapProjection, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }));
        }
    }
    MapSystemTrafficLayer.DRAW_GROUPS = [
        { alertLevelVisFlag: exports.MapTrafficAlertLevelVisibility.Other, alertLevel: exports.TcasAlertLevel.None },
        { alertLevelVisFlag: exports.MapTrafficAlertLevelVisibility.ProximityAdvisory, alertLevel: exports.TcasAlertLevel.ProximityAdvisory },
        { alertLevelVisFlag: exports.MapTrafficAlertLevelVisibility.TrafficAdvisory, alertLevel: exports.TcasAlertLevel.TrafficAdvisory },
        { alertLevelVisFlag: exports.MapTrafficAlertLevelVisibility.ResolutionAdvisory, alertLevel: exports.TcasAlertLevel.ResolutionAdvisory },
    ];
    /**
     * An abstract implementation of {@link MapTrafficIntruderIcon} which handles the projection of the intruder's position
     * and off-scale calculations.
     */
    class AbstractMapTrafficIntruderIcon {
        /**
         * Constructor.
         * @param intruder This icon's associated intruder.
         * @param trafficModule The traffic module for this icon's parent map.
         * @param ownshipModule The ownship module for this icon's parent map.
         */
        constructor(intruder, trafficModule, ownshipModule) {
            this.intruder = intruder;
            this.trafficModule = trafficModule;
            this.ownshipModule = ownshipModule;
            this.projectedPos = new Float64Array(2);
            this.isOffScale = false;
        }
        /**
         * Draws this icon.
         * @param projection The map projection.
         * @param context The canvas rendering context to which to draw this icon.
         * @param offScaleRange The distance from the own airplane to this icon's intruder beyond which the intruder is
         * considered off-scale. If the value is `NaN`, the intruder is never considered off-scale.
         */
        draw(projection, context, offScaleRange) {
            this.updatePosition(projection, offScaleRange);
            this.drawIcon(projection, context, this.projectedPos, this.isOffScale);
        }
        /**
         * Updates this icon's intruder's projected position and off-scale status.
         * @param projection The map projection.
         * @param offScaleRange The distance from the own airplane to this icon's intruder beyond which the intruder is
         * considered off-scale. If the value is `NaN`, the intruder is never considered off-scale.
         */
        updatePosition(projection, offScaleRange) {
            const ownAirplanePos = this.ownshipModule.position.get();
            if (offScaleRange.isNaN()) {
                projection.project(this.intruder.position, this.projectedPos);
                this.isOffScale = false;
            }
            else {
                this.handleOffScaleRange(projection, ownAirplanePos, offScaleRange);
            }
        }
        /**
         * Updates this icon's intruder's projected position and off-scale status using a specific range from the own
         * airplane to define off-scale.
         * @param projection The map projection.
         * @param ownAirplanePos The position of the own airplane.
         * @param offScaleRange The distance from the own airplane to this icon's intruder beyond which the intruder is
         * considered off-scale.
         */
        handleOffScaleRange(projection, ownAirplanePos, offScaleRange) {
            const intruderPos = this.intruder.position;
            const horizontalSeparation = intruderPos.distance(ownAirplanePos);
            const offscaleRangeRad = offScaleRange.asUnit(UnitType.GA_RADIAN);
            if (horizontalSeparation > offscaleRangeRad) {
                this.isOffScale = true;
                projection.project(ownAirplanePos.offset(ownAirplanePos.bearingTo(intruderPos), offscaleRangeRad, AbstractMapTrafficIntruderIcon.geoPointCache[0]), this.projectedPos);
            }
            else {
                this.isOffScale = false;
                projection.project(intruderPos, this.projectedPos);
            }
        }
    }
    AbstractMapTrafficIntruderIcon.geoPointCache = [new GeoPoint(0, 0)];

    /**
     * A class that renders waypoints into a layer.
     */
    class MapSystemWaypointsLayer extends MapLayer {
        constructor() {
            var _a;
            super(...arguments);
            this.waypointsLayer = exports.FSComponent.createRef();
            this.displayModule = this.props.model.getModule(MapSystemKeys.NearestWaypoints);
            this.waypointCache = (_a = this.props.waypointCache) !== null && _a !== void 0 ? _a : DefaultFacilityWaypointCache.getCache(this.props.bus);
            this.searchItemLimits = {
                [exports.FacilitySearchType.Airport]: 500,
                [exports.FacilitySearchType.Vor]: 250,
                [exports.FacilitySearchType.Ndb]: 250,
                [exports.FacilitySearchType.Intersection]: 500,
                [exports.FacilitySearchType.User]: 100
            };
            this.searchRadiusLimits = {
                [exports.FacilitySearchType.Airport]: Number.POSITIVE_INFINITY,
                [exports.FacilitySearchType.Vor]: Number.POSITIVE_INFINITY,
                [exports.FacilitySearchType.Ndb]: Number.POSITIVE_INFINITY,
                [exports.FacilitySearchType.Intersection]: Number.POSITIVE_INFINITY,
                [exports.FacilitySearchType.User]: Number.POSITIVE_INFINITY
            };
        }
        /** @inheritdoc */
        onAttached() {
            super.onAttached();
            this.waypointsLayer.instance.onAttached();
            this.initEventHandlers();
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            super.onMapProjectionChanged(mapProjection, changeFlags);
            this.waypointsLayer.instance.onMapProjectionChanged(mapProjection, changeFlags);
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            if (this.isVisible()) {
                this.waypointsLayer.instance.onUpdated(time, elapsed);
            }
        }
        /** @inheritdoc */
        initEventHandlers() {
            this.displayModule.numAirports.sub(num => this.searchItemLimits[exports.FacilitySearchType.Airport] = num, true);
            this.displayModule.numIntersections.sub(num => this.searchItemLimits[exports.FacilitySearchType.Intersection] = num, true);
            this.displayModule.numVors.sub(num => this.searchItemLimits[exports.FacilitySearchType.Vor] = num, true);
            this.displayModule.numNdbs.sub(num => this.searchItemLimits[exports.FacilitySearchType.Ndb] = num, true);
            this.displayModule.airportsRange.sub(num => this.searchRadiusLimits[exports.FacilitySearchType.Airport] = num.asUnit(UnitType.GA_RADIAN), true);
            this.displayModule.intersectionsRange.sub(num => this.searchRadiusLimits[exports.FacilitySearchType.Intersection] = num.asUnit(UnitType.GA_RADIAN), true);
            this.displayModule.vorsRange.sub(num => this.searchRadiusLimits[exports.FacilitySearchType.Vor] = num.asUnit(UnitType.GA_RADIAN), true);
            this.displayModule.ndbsRange.sub(num => this.searchRadiusLimits[exports.FacilitySearchType.Ndb] = num.asUnit(UnitType.GA_RADIAN), true);
            this.displayModule.refreshWaypoints.on(() => this.waypointsLayer.instance.refreshWaypoints());
        }
        /**
         * A callback called when the nearest facility search sessions have been started.
         * @param airportSession The airport search session.
         * @param vorSession The VOR search session.
         * @param ndbSession The NDB search session.
         * @param intSession The intersection search session.
         * @param usrSession The user facility search session.
         */
        onSessionsStarted(airportSession, vorSession, ndbSession, intSession, usrSession) {
            this.displayModule.intersectionsFilter.sub(filters => intSession.setIntersectionFilter(filters.typeMask, filters.showTerminalWaypoints), true);
            this.displayModule.vorsFilter.sub(filters => vorSession.setVorFilter(filters.classMask, filters.typeMask), true);
            this.displayModule.airportsFilter.sub(filters => {
                airportSession.setAirportFilter(filters.showClosed, filters.classMask);
            }, true);
            this.displayModule.extendedAirportsFilter.sub(filters => {
                airportSession.setExtendedAirportFilters(filters.runwaySurfaceTypeMask, filters.approachTypeMask, filters.toweredMask, filters.minimumRunwayLength);
            }, true);
            this.displayModule.usersFilter.sub(filter => {
                usrSession.setFacilityFilter(filter);
            });
        }
        /**
         * Initializes this layer's waypoint renderer.
         * @param renderer This layer's waypoint renderer.
         * @param canvasLayer The canvas layer to which to draw the waypoints.
         */
        initWaypointRenderer(renderer, canvasLayer) {
            this.defineRenderRole(renderer, canvasLayer);
            renderer.onRolesAdded.on(this.defineRenderRole.bind(this, renderer, canvasLayer));
        }
        /**
         * Defines the render role for this layer's waypoints.
         * @param renderer This layer's waypoint renderer.
         * @param canvasLayer The canvas layer to which to draw the waypoints.
         */
        defineRenderRole(renderer, canvasLayer) {
            const groupRoles = renderer.getRoleNamesByGroup(exports.MapSystemWaypointRoles.Normal);
            groupRoles.forEach(id => {
                const roleId = renderer.getRoleFromName(id);
                if (roleId !== undefined) {
                    renderer.setCanvasContext(roleId, canvasLayer.display.context);
                    renderer.setIconFactory(roleId, this.props.iconFactory);
                    renderer.setLabelFactory(roleId, this.props.labelFactory);
                    renderer.setVisibilityHandler(roleId, this.isWaypointVisible.bind(this));
                }
            });
        }
        /** @inheritdoc */
        setVisible(val) {
            super.setVisible(val);
            this.waypointsLayer.instance.setVisible(val);
        }
        /**
         * Checks to see if a waypoint should be visible.
         * @param waypoint The waypoint to check.
         * @returns True if visible, false otherwise.
         */
        isWaypointVisible(waypoint) {
            if (FacilityWaypointUtils.isFacilityWaypoint(waypoint)) {
                switch (waypoint.type) {
                    case exports.WaypointTypes.Airport:
                        return this.displayModule.showAirports.get()(waypoint);
                    case exports.WaypointTypes.Intersection:
                        return this.displayModule.showIntersections.get()(waypoint);
                    case exports.WaypointTypes.VOR:
                        return this.displayModule.showVors.get()(waypoint);
                    case exports.WaypointTypes.NDB:
                        return this.displayModule.showNdbs.get()(waypoint);
                }
            }
            return false;
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent(MapNearestWaypointsLayer, { ref: this.waypointsLayer, model: this.props.model, mapProjection: this.props.mapProjection, bus: this.props.bus, waypointRenderer: this.props.waypointRenderer, waypointForFacility: (facility) => this.waypointCache.get(facility), initRenderer: this.initWaypointRenderer.bind(this), registerWaypoint: this.registerWaypoint.bind(this), deregisterWaypoint: this.deregisterWaypoint.bind(this), searchItemLimit: (type) => this.searchItemLimits[type], searchRadiusLimit: (type) => this.searchRadiusLimits[type], getSearchCenter: this.props.useMapTargetAsSearchCenter === true ? (mapProjection) => mapProjection.getTarget() : undefined, onSessionsStarted: this.onSessionsStarted.bind(this), class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }));
        }
        /**
         * Registers a waypoint with this layer's waypoint renderer.
         * @param waypoint The waypoint to register.
         * @param renderer The renderer to register the waypoint to.
         */
        registerWaypoint(waypoint, renderer) {
            const selector = this.displayModule.waypointRoleSelector.get();
            if (selector) {
                const id = selector(waypoint);
                const roleId = renderer.getRoleFromName(id);
                if (roleId !== undefined) {
                    renderer.register(waypoint, roleId, 'waypoints-layer');
                }
            }
            else {
                const groupRoles = renderer.getRoleNamesByGroup(exports.MapSystemWaypointRoles.Normal);
                groupRoles.forEach(id => {
                    const roleId = renderer.getRoleFromName(id);
                    if (roleId !== undefined) {
                        renderer.register(waypoint, roleId, 'waypoints-layer');
                    }
                });
            }
        }
        /**
         * Deregisters a waypoint with this layer's waypoint renderer.
         * @param waypoint The waypoint to deregister.
         * @param renderer The renderer to deregister the waypoint from.
         */
        deregisterWaypoint(waypoint, renderer) {
            const groupRoles = renderer.getRoleNamesByGroup(exports.MapSystemWaypointRoles.Normal);
            groupRoles.forEach(id => {
                const roleId = renderer.getRoleFromName(id);
                if (roleId !== undefined) {
                    renderer.deregister(waypoint, roleId, 'waypoints-layer');
                }
            });
        }
    }

    /**
     * A map data module that controls the terrain color reference point.
     */
    class MapTerrainColorsModule {
        constructor() {
            /** The terrain colors reference point. */
            this.reference = Subject.create(EBingReference.SEA);
            /** Whether or not to show the map terrain isolines. */
            this.showIsoLines = Subject.create(false);
            /** The terrain colors array. */
            this.colors = ArraySubject.create(BingComponent.createEarthColorsArray('#0000FF', [
                { elev: 0, color: '#000000' }
            ], 0, 30000, 1));
            /** The elevation range over which to assign the terrain colors, as `[minimum, maximum]` in feet. */
            this.colorsElevationRange = Vec2Subject.create(Vec2Math.create(0, 30000));
        }
    }

    /**
     * A map data module that handles the display of flight plan data.
     */
    class MapFlightPlanModule {
        constructor() {
            this.plans = [];
        }
        /**
         * Gets the flight plan subjects for a specified flight plan.
         * @param index The index of the flight plan.
         * @returns The subject for the specified plan index.
         */
        getPlanSubjects(index) {
            let planSubject = this.plans[index];
            if (planSubject === undefined) {
                planSubject = new PlanSubjects();
                this.plans[index] = planSubject;
            }
            return planSubject;
        }
    }
    /**
     * A collection of subjects for consuming flight plan data in the flight plan module.
     */
    class PlanSubjects {
        constructor() {
            /** The current flight plan to display, if any. */
            this.flightPlan = Subject.create(undefined);
            /** An event that fires when the plan is changed. */
            this.planChanged = new SubEvent();
            /** An event that fired when the flight path of the plan is recalculated. */
            this.planCalculated = new SubEvent();
            /** The active leg index currently being navigated to. */
            this.activeLeg = Subject.create(0);
        }
    }

    /**
     * A module which describes whether the map is following the player airplane.
     */
    class MapFollowAirplaneModule {
        constructor() {
            /** Whether the map is following the player airplane. */
            this.isFollowing = Subject.create(false);
        }
    }

    /**
     * A map data module that controls waypoint display options.
     */
    class MapWaypointDisplayModule {
        constructor() {
            /** A handler that dictates airport waypoint visibility. */
            this.showAirports = Subject.create(() => true);
            /** A handler that dictates intersection waypoint visibility. */
            this.showIntersections = Subject.create(() => false);
            /** A handler that dictates NDB waypoint visibility. */
            this.showNdbs = Subject.create(() => true);
            /** A handler that dictates VOR waypoint visibility. */
            this.showVors = Subject.create(() => true);
            /** The maximum range at which airport waypoints should be searched for. */
            this.airportsRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(50));
            /** The maximum range at which intersection waypoints should be searched for. */
            this.intersectionsRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(50));
            /** The maximum range at which NDB waypoints should be searched for. */
            this.ndbsRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(500));
            /** The maximum range at which VOR waypoints should be searched for. */
            this.vorsRange = NumberUnitSubject.createFromNumberUnit(UnitType.NMILE.createNumber(500));
            /** The maximum number of airports that should be displayed. */
            this.numAirports = Subject.create(40);
            /** The maximum number of intersections that should be displayed. */
            this.numIntersections = Subject.create(40);
            /** The maximum number of NDBs that should be displayed. */
            this.numNdbs = Subject.create(40);
            /** The maximum number of VORs that should be displayed. */
            this.numVors = Subject.create(40);
            /** The filter to apply to the intersection search. */
            this.intersectionsFilter = Subject.create({
                typeMask: NearestIntersectionSearchSession.Defaults.TypeMask,
                showTerminalWaypoints: true
            });
            /** The filter to apply to the VOR search. */
            this.vorsFilter = Subject.create({
                typeMask: NearestVorSearchSession.Defaults.TypeMask,
                classMask: NearestVorSearchSession.Defaults.ClassMask
            });
            /** The filter to apply to the airport search. */
            this.airportsFilter = Subject.create({
                classMask: NearestAirportSearchSession.Defaults.ClassMask,
                showClosed: NearestAirportSearchSession.Defaults.ShowClosed
            });
            /** The extended airport filter to apply to the airport search. */
            this.extendedAirportsFilter = Subject.create({
                runwaySurfaceTypeMask: NearestAirportSearchSession.Defaults.SurfaceTypeMask,
                approachTypeMask: NearestAirportSearchSession.Defaults.ApproachTypeMask,
                minimumRunwayLength: NearestAirportSearchSession.Defaults.MinimumRunwayLength,
                toweredMask: NearestAirportSearchSession.Defaults.ToweredMask
            });
            /** The filter to apply to the user facility search. */
            this.usersFilter = Subject.create(undefined);
            /** A function that will be called with a waypoint when it is registered,
             * and should return the role to use for that waypoint. */
            this.waypointRoleSelector = Subject.create(undefined);
            /** Forces a refresh of all the waypoints. Useful if a waypoint needs a different role to be selected. */
            this.refreshWaypoints = new SubEvent();
        }
    }

    /**
     * A module that describes the display of weather on a Bing Map instance.
     */
    class MapWxrModule {
        constructor() {
            /** Whether the weather radar is enabled. */
            this.isEnabled = Subject.create(false);
            /** The current map weather radar arc sweep angle in degrees. */
            this.weatherRadarArc = NumberUnitSubject.create(UnitType.DEGREE.createNumber(90));
            /** The current weather radar mode. */
            this.weatherRadarMode = Subject.create(EWeatherRadar.HORIZONTAL);
            /**
             * The current weather radar colors. Each entry `E_i` of the array is a tuple `[color, rate]` that defines a color
             * stop, where `color` is an RGBA color expressed as `R + G * 256 + B * 256^2 + A * 256^3` and `rate` is a
             * precipitation rate in millimeters per hour.
             *
             * In general, the color defined by `E_i` is applied to precipitation rates ranging from the rate defined by `E_i-1`
             * to the rate defined by `E_i`. There are two special cases. The color defined by `E_0` is applied to the
             * precipitation rates from zero to the rate defined by `E_0`. The color defined by `E_n-1`, where `n` is the length
             * of the array, is applied to the precipitation rates from the rate defined by `E_n-2` to positive infinity.
             */
            this.weatherRadarColors = ArraySubject.create(Array.from(BingComponent.DEFAULT_WEATHER_COLORS));
            this._wxrMode = MappedSubject.create(([isEnabled, arc, mode]) => {
                return {
                    mode: isEnabled ? mode : EWeatherRadar.OFF,
                    arcRadians: arc.asUnit(UnitType.RADIAN),
                };
            }, this.isEnabled, this.weatherRadarArc, this.weatherRadarMode);
        }
        /**
         * A subscribable containing the combined WxrMode from the mode and arc subjects,
         * suitable for consumption in a MapBingLayer.
         * @returns The WxrMode subscribable.
         */
        get wxrMode() {
            return this._wxrMode;
        }
    }

    /**
     * A module describing the display of the altitude intercept arc.
     */
    class MapAltitudeArcModule {
        /**
         * MapAltitudeArcModule constructor.
         * @param show When passed, the `show` field will be initialized to this value.
         */
        constructor(show) {
            /** Whether to show the altitude intercept arc. */
            this.show = Subject.create(false);
            if (show !== undefined) {
                this.show.set(show);
            }
        }
    }

    /**
     * A class that builds a configuration for the waypoint display.
     */
    class WaypointDisplayBuilder {
        /**
         * Creates an instance of the WaypointDisplayBuilder.
         * @param iconFactory The icon factory to use with this builder.
         * @param labelFactory The label factory to use with this builder.
         * @param waypointRenderer The waypoint renderer to use with this builder.
         */
        constructor(iconFactory, labelFactory, waypointRenderer) {
            this.iconFactory = iconFactory;
            this.labelFactory = labelFactory;
            this.waypointRenderer = waypointRenderer;
            this.roleGroup = exports.MapSystemWaypointRoles.Normal;
            this.isCenterTarget = false;
        }
        /**
         * Adds a icon configuration to the waypoint display system.
         * @param role The role to add this waypoint display config for.
         * @param type The type of waypoint to add an icon for.
         * @param config The waypoint icon factory to add as a configuration.
         * @returns The modified builder.
         */
        addIcon(role, type, config) {
            this.iconFactory.addIconFactory(this.determineRoleId(role), type, config);
            return this;
        }
        /**
         * Adds a default icon configuration to the waypoint display system, if no other configuration is found.
         * @param role The role to add this waypoint display config for.
         * @param config The waypoint icon factory to add as a configuration.
         * @returns The modified builder.
         */
        addDefaultIcon(role, config) {
            this.iconFactory.addDefaultIconFactory(this.determineRoleId(role), config);
            return this;
        }
        /**
         * Adds a label configuration to the waypoint display system.
         * @param role The role to add this waypoint display config for.
         * @param type The type of waypoint to add an label for.
         * @param config The waypoint label factory to add as a configuration.
         * @returns The modified builder.
         */
        addLabel(role, type, config) {
            this.labelFactory.addLabelFactory(this.determineRoleId(role), type, config);
            return this;
        }
        /**
         * Adds a label configuration to the waypoint display system.
         * @param role The role to add this waypoint display config for.
         * @param config The waypoint label factory to add as a configuration.
         * @returns The modified builder.
         */
        addDefaultLabel(role, config) {
            this.labelFactory.addDefaultLabelFactory(this.determineRoleId(role), config);
            return this;
        }
        /**
         * Determines the role ID given either a numeric or string based role.
         * @param role The role to determine.
         * @returns The numeric role ID.
         */
        determineRoleId(role) {
            let roleId = 0;
            if (typeof role === 'string') {
                const roleIdFromName = this.waypointRenderer.getRoleFromName(role);
                if (roleIdFromName !== undefined) {
                    roleId = roleIdFromName;
                }
            }
            else {
                roleId = role;
            }
            return roleId;
        }
        /**
         * Registers a waypoint display role for use with the flight plan rendering
         * system.
         * @param name The name of the role to register.
         * @returns The modified builder.
         */
        registerRole(name) {
            this.waypointRenderer.addRenderRole(name, undefined, this.roleGroup);
            return this;
        }
        /**
         * Gets the ID of a role in the waypoint display system.
         * @param role The name of the role to get the ID for.
         * @returns The ID of the role.
         * @throws An error if an invalid role name is supplied.
         */
        getRoleId(role) {
            const roleId = this.waypointRenderer.getRoleFromName(role);
            if (roleId === undefined) {
                throw new Error(`The role with name ${role} was not defined and could not be found.`);
            }
            return roleId;
        }
        /**
         * Configures the center for waypoint searches for this display.
         * @param center If center, then waypoint searches will use the map center. If target,
         * waypoint searches will use the map target with offset.
         * @returns The modified builder.
         */
        withSearchCenter(center) {
            if (center === 'center') {
                this.isCenterTarget = false;
            }
            else {
                this.isCenterTarget = true;
            }
            return this;
        }
        /**
         * Gets if the waypoint search is using the map target with offset as the search center.
         * @returns True if the search center is the map target, false if it is the map center.
         */
        getIsCenterTarget() {
            return this.isCenterTarget;
        }
        /**
         * Configures the facility waypoint cache to use with the waypoint display.
         * @param cache The facility waypoint cache to use, or undefined to use the default.
         * @returns The modified builder.
         */
        withWaypointCache(cache) {
            this.facilityWaypointCache = cache;
            return this;
        }
        /**
         * Gets the currently set facility waypoint cache.
         * @returns The currently set facility waypoint cache.
         */
        getWaypointCache() {
            return this.facilityWaypointCache;
        }
    }

    /**
     * A class that builds the configuration for the flight plan display.
     */
    class FlightPlanDisplayBuilder extends WaypointDisplayBuilder {
        /**
         * Creates an instance of the FlightPlanDisplayBuilder.
         * @param iconFactory The icon factory to use with this builder.
         * @param labelFactory The label factory to use with this builder.
         * @param waypointRenderer The waypoint renderer to use with this builder.
         * @param flightPlanRenderer The flight plan renderer to use with this builder.
         * @param planIndex The flight plan index to be displayed by this system.
         */
        constructor(iconFactory, labelFactory, waypointRenderer, flightPlanRenderer, planIndex) {
            super(iconFactory, labelFactory, waypointRenderer);
            this.flightPlanRenderer = flightPlanRenderer;
            this.planIndex = planIndex;
            this.roleGroup = exports.MapSystemWaypointRoles.FlightPlan;
            this.roleGroup = `${exports.MapSystemWaypointRoles.FlightPlan}_${planIndex}`;
            flightPlanRenderer.legStyleHandlers;
        }
        /**
         * Registers a waypoint display role for use with the flight plan rendering
         * system.
         * @param name The name of the role to register.
         * @returns The modified builder.
         */
        registerRole(name) {
            this.waypointRenderer.insertRenderRole(name, exports.MapSystemWaypointRoles.Normal, undefined, this.roleGroup);
            return this;
        }
        /**
         * Configures the flight path display to use styles returned by the provided function.
         * @param handler The handler to use to return the required path rendering styles.
         * @returns The modified builder.
         */
        withLegPathStyles(handler) {
            this.flightPlanRenderer.legStyleHandlers.set(this.planIndex, handler);
            return this;
        }
        /**
         * Configures the flight plan waypoint display to use the roles returned by the
         * provided function.
         * @param handler The handler to use to return the required waypoint display roles.
         * @returns The modified builder.
         */
        withLegWaypointRoles(handler) {
            this.flightPlanRenderer.legWaypointHandlers.set(this.planIndex, handler);
            return this;
        }
        /**
         * Configures the flight plan flight path display to set visibility of leg-to-leg
         * turn anticipation transitions.
         * @param visible Whether or not the leg-to-leg turn anticipation transitions will be visible.
         * @returns The modified builder.
         */
        withAnticipationTurns(visible) {
            const visibleSub = SubscribableUtils.toSubscribable(visible, true);
            this.flightPlanRenderer.renderEgress = visibleSub;
            this.flightPlanRenderer.renderIngress = visibleSub;
            return this;
        }
    }

    /**
     * A heap which allocates instances of a resource.
     */
    class ResourceHeap {
        /**
         * Constructor.
         * @param factory A function which creates new instances of this heap's resource.
         * @param destructor A function which destroys instances of this heap's resource.
         * @param onAllocated A function which is called when an instance of this heap's resource is allocated.
         * @param onFreed A function which is called when an instance of this heap's resource is freed.
         * @param initialSize The initial size of this heap. Defaults to `0`.
         * @param maxSize The maximum size of this heap. Defaults to `Number.MAX_SAFE_INTEGER`. This heap cannot allocate
         * more resources than its maximum size.
         * @param autoShrinkThreshold The size above which this heap will attempt to automatically reduce its size when
         * resources are freed. The heap will never reduce its size below this threshold. Defaults to
         * `Number.MAX_SAFE_INTEGER`.
         */
        constructor(factory, destructor, onAllocated, onFreed, initialSize = 0, maxSize = Number.MAX_SAFE_INTEGER, autoShrinkThreshold = Number.MAX_SAFE_INTEGER) {
            this.factory = factory;
            this.destructor = destructor;
            this.onAllocated = onAllocated;
            this.onFreed = onFreed;
            this.maxSize = maxSize;
            this.autoShrinkThreshold = autoShrinkThreshold;
            this.cache = [];
            this.numAllocated = 0;
            for (let i = 0; i < Math.min(initialSize, maxSize); i++) {
                this.cache.push(factory());
            }
        }
        /**
         * Allocates a resource instance from this heap. If this heap has an existing free resource available, one will be
         * returned. Otherwise, a new resource instance will be created, added to the heap, and returned.
         * @returns A resource.
         * @throws Error if this heap has reached its allocation limit.
         */
        allocate() {
            if (this.numAllocated >= this.maxSize) {
                throw new Error(`ResourceHeap: maximum number of allocations (${this.maxSize}) reached`);
            }
            let resource;
            if (this.numAllocated < this.cache.length) {
                resource = this.cache[this.numAllocated];
            }
            else {
                this.cache.push(resource = this.factory());
            }
            this.numAllocated++;
            if (this.onAllocated !== undefined) {
                this.onAllocated(resource);
            }
            return resource;
        }
        /**
         * Frees a resource instance allocated from this heap, allowing it to be re-used.
         * @param resource The resource to free.
         */
        free(resource) {
            const index = this.cache.indexOf(resource);
            if (index < 0 || index >= this.numAllocated) {
                return;
            }
            const freed = this.cache[index];
            this.numAllocated--;
            this.cache[index] = this.cache[this.numAllocated];
            this.cache[this.numAllocated] = freed;
            // If the heap size is over the auto-shrink threshold and the number of allocated instances drops to less than or
            // equal to half of the heap size, then reduce the size of the heap to the threshold, or 125% of the number of
            // allocated instances, whichever is greater.
            if (this.cache.length > this.autoShrinkThreshold && this.numAllocated <= this.cache.length / 2) {
                const newLength = Math.max(this.autoShrinkThreshold, this.numAllocated * 1.25);
                for (let i = newLength; i < this.cache.length; i++) {
                    this.destructor(this.cache[i]);
                }
                this.cache.length = newLength;
            }
            if (this.onFreed !== undefined) {
                this.onFreed(resource);
            }
        }
    }

    /**
     * Moderates access to a resource.
     */
    class ResourceModerator {
        /**
         * Constructor.
         * @param resource This resource controlled by this moderator.
         */
        constructor(resource) {
            this.resource = resource;
            this.pendingConsumer = null;
            this.assignedConsumer = null;
            this.queuedConsumers = new SortedArray((a, b) => a.priority - b.priority);
        }
        /**
         * Makes a claim to this moderator's resource. If the resource is not currently owned, or the claiming consumer has
         * a higher priority than the current owner, access will attempt to pass to the claiming consumer. Otherwise, the
         * claiming consumer will enter a queue. After entering the queue, a consumer will gain access to the claimed
         * resource when all other consumers with a higher priority forfeit their claims to the resource.
         * @param consumer The consumer claiming the resource.
         */
        claim(consumer) {
            var _a;
            const consumerToDisplace = (_a = this.pendingConsumer) !== null && _a !== void 0 ? _a : this.assignedConsumer;
            if (consumerToDisplace === consumer) {
                return;
            }
            if (this.queuedConsumers.has(consumer)) {
                return;
            }
            if (consumerToDisplace === null || consumerToDisplace.priority < consumer.priority) {
                if (consumerToDisplace === null || consumerToDisplace === this.pendingConsumer) {
                    if (consumerToDisplace) {
                        this.queuedConsumers.insert(consumerToDisplace);
                        this.pendingConsumer = null;
                    }
                    this.assignedConsumer = consumer;
                    this.assignedConsumer.onAcquired(this.resource);
                    return;
                }
                this.assignedConsumer = null;
                this.pendingConsumer = consumer;
                this.queuedConsumers.insert(consumerToDisplace);
                consumerToDisplace.onCeded(this.resource);
                if (this.pendingConsumer === consumer) {
                    this.pendingConsumer = null;
                    this.assignedConsumer = consumer;
                    this.assignedConsumer.onAcquired(this.resource);
                    return;
                }
                else {
                    // Something has displaced the new pending consumer. Either something pre-empted it, in which case it is
                    // now in the queue, or it forfeited its claim. In either case, there is nothing to do.
                    return;
                }
            }
            this.queuedConsumers.insert(consumer);
        }
        /**
         * Forfeits a claim to this moderator's resource. If the consumer forfeiting its claim is the current owner of the
         * resource, it will immediately lose access to the resource, and access will attempt to pass to the next-highest
         * priority consumer with a claim to the resource. Otherwise, the forfeiting consumer will be removed from the queue
         * to gain access to the resource.
         * @param consumer The consumer that is forfeiting its claim.
         */
        forfeit(consumer) {
            var _a;
            if (this.pendingConsumer === consumer) {
                this.pendingConsumer = null;
                return;
            }
            if (this.assignedConsumer === null || this.assignedConsumer !== consumer) {
                this.queuedConsumers.remove(consumer);
                return;
            }
            const next = (_a = this.queuedConsumers.pop()) !== null && _a !== void 0 ? _a : null;
            this.pendingConsumer = next;
            this.assignedConsumer = null;
            consumer.onCeded(this.resource);
            if (next !== null && this.pendingConsumer === next) {
                this.pendingConsumer = null;
                this.assignedConsumer = next;
                this.assignedConsumer.onAcquired(this.resource);
            }
        }
    }

    /**
     * An generic implementation of {@link MapAirspaceRenderManager}.
     */
    class GenericAirspaceRenderManager {
        /**
         * Constructor.
         * @param renderOrder A function which determines the order in which this manager renders airspaces. The function
         * should return a negative number when airspace `a` should be rendered before (below) airspace `b`, a positive
         * number when airspace `a` should be rendered after (above) airspace `b`, and `0` when the relative render order
         * of the two airspaces does not matter.
         * @param selectRenderer A function which selects airspace renderers for individual airspaces.
         */
        constructor(renderOrder, selectRenderer) {
            this.renderOrder = renderOrder;
            this.selectRenderer = selectRenderer;
            this.airspaces = new Map();
        }
        /** @inheritdoc */
        getRegisteredAirspaces() {
            return Array.from(this.airspaces.values());
        }
        /** @inheritdoc */
        registerAirspace(airspace) {
            if (this.airspaces.has(airspace.facility.id)) {
                return false;
            }
            this.airspaces.set(airspace.facility.id, airspace);
            return true;
        }
        /** @inheritdoc */
        deregisterAirspace(airspace) {
            return this.airspaces.delete(airspace.facility.id);
        }
        /** @inheritdoc */
        replaceRegisteredAirspaces(airspaces) {
            let changed = false;
            let numMatched = 0;
            for (const airspace of airspaces) {
                changed || (changed = !this.airspaces.has(airspace.facility.id));
                if (changed) {
                    break;
                }
                else {
                    numMatched++;
                }
            }
            changed || (changed = numMatched !== this.airspaces.size);
            if (!changed) {
                return false;
            }
            this.airspaces.clear();
            for (const airspace of airspaces) {
                this.registerAirspace(airspace);
            }
            return true;
        }
        /** @inheritdoc */
        clearRegisteredAirspaces() {
            if (this.airspaces.size === 0) {
                return false;
            }
            this.airspaces.clear();
            return true;
        }
        /** @inheritdoc */
        prepareRenderProcess(projection, context, taskQueueHandler, lod = 0, stream) {
            const sorted = Array.from(this.airspaces.values()).sort(this.renderOrder);
            const tasks = sorted.map(airspace => {
                const renderer = this.selectRenderer(airspace);
                // The explicit cast is to avoid a bogus typescript error
                return renderer.render.bind(renderer, airspace, projection, context, lod, stream);
            });
            return new ThrottledTaskQueueProcess(new ArrayTaskQueue(tasks), taskQueueHandler);
        }
    }

    /**
     * Updates the properties in a {@link MapAutopilotPropsModule}.
     */
    class MapAutopilotPropsController extends MapSystemController {
        /**
         * Constructor.
         * @param context This controller's map context.
         * @param properties The properties to update on the module.
         * @param updateFreq The update frequency, in hertz. If not defined, the properties will be updated every frame.
         */
        constructor(context, properties, updateFreq) {
            super(context);
            this.properties = properties;
            this.module = this.context.model.getModule(MapSystemKeys.AutopilotProps);
            this.subs = [];
            this.updateFreq = updateFreq === undefined ? undefined : SubscribableUtils.toSubscribable(updateFreq, true);
        }
        /** @inheritdoc */
        onAfterMapRender() {
            const sub = this.context.bus.getSubscriber();
            if (this.updateFreq) {
                this.updateFreqSub = this.updateFreq.sub(freq => {
                    for (const subscription of this.subs) {
                        subscription.destroy();
                    }
                    this.subs.length = 0;
                    for (const property of this.properties) {
                        this.subs.push(this.bindProperty(sub, property, freq));
                    }
                }, true);
            }
            else {
                for (const property of this.properties) {
                    this.subs.push(this.bindProperty(sub, property));
                }
            }
        }
        /**
         * Binds a module property to data received through the event bus.
         * @param sub The event bus subscriber.
         * @param property The property to bind.
         * @param updateFreq The data update frequency.
         * @returns The subscription created by the binding.
         * @throws Error if the property is invalid.
         */
        bindProperty(sub, property, updateFreq) {
            let key;
            let topic = undefined;
            if (typeof property === 'string') {
                key = property;
            }
            else {
                key = property.key;
                topic = property.topic;
            }
            switch (key) {
                case 'selectedAltitude':
                    topic !== null && topic !== void 0 ? topic : (topic = 'ap_altitude_selected');
                    return (updateFreq === undefined ? sub.on(topic) : sub.on(topic).atFrequency(updateFreq))
                        .handle(alt => { this.module.selectedAltitude.set(alt, UnitType.FOOT); });
                case 'selectedHeading':
                    topic !== null && topic !== void 0 ? topic : (topic = 'ap_heading_selected');
                    return (updateFreq === undefined ? sub.on(topic) : sub.on(topic).atFrequency(updateFreq))
                        .handle(hdg => { this.module.selectedHeading.set(hdg); });
                default:
                    throw new Error(`MapAutopilotPropsController: invalid property key: ${key}`);
            }
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.destroy();
        }
        /** @inheritdoc */
        destroy() {
            var _a;
            (_a = this.updateFreqSub) === null || _a === void 0 ? void 0 : _a.destroy();
            for (const sub of this.subs) {
                sub.destroy();
            }
            super.destroy();
        }
    }

    /**
     * A component that encompasses the compiled map system.
     */
    class MapSystemComponent extends MapComponent {
        /** @inheritdoc */
        constructor(props) {
            var _a;
            super(props);
            this.rootStyles = ObjectSubject.create({
                width: '0px',
                height: '0px'
            });
            this.deadZone = (_a = this.props.deadZone) !== null && _a !== void 0 ? _a : VecNSubject.create(new Float64Array(4));
            this.deadZone.sub(this.onDeadZoneChanged.bind(this));
        }
        /** @inheritdoc */
        onAfterRender(thisNode) {
            super.onAfterRender(thisNode);
            this.onProjectedSizeChanged();
            this.props.onAfterRender();
        }
        /**
         * This method is called when the size of this map's dead zone changes.
         * @param deadZone The dead zone.
         */
        onDeadZoneChanged(deadZone) {
            this.props.onDeadZoneChanged(deadZone);
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            super.onMapProjectionChanged(mapProjection, changeFlags);
            this.props.onMapProjectionChanged(mapProjection, changeFlags);
        }
        /**
         * Sets the size of this map's root HTML element.
         * @param size The new size, in pixels.
         */
        setRootSize(size) {
            this.rootStyles.set('width', `${size[0]}px`);
            this.rootStyles.set('height', `${size[1]}px`);
        }
        /** @inheritdoc */
        onProjectedSizeChanged() {
            this.setRootSize(this.mapProjection.getProjectedSize());
        }
        /** @inheritdoc */
        onUpdated(time, elapsed) {
            this.props.onBeforeUpdated(time, elapsed);
            super.onUpdated(time, elapsed);
            this.props.onAfterUpdated(time, elapsed);
        }
        /** @inheritdoc */
        onWake() {
            super.onWake();
            this.props.onWake();
        }
        /** @inheritdoc */
        onSleep() {
            super.onSleep();
            this.props.onSleep();
        }
        /** @inheritdoc */
        render() {
            var _a;
            return (exports.FSComponent.buildComponent("div", { style: this.rootStyles, class: (_a = this.props.class) !== null && _a !== void 0 ? _a : '' }, this.props.children));
        }
        /** @inheritdoc */
        destroy() {
            super.destroy();
            this.props.onDestroy();
        }
    }

    /**
     * An implementation of the base properties in {@link MapSystemContext}.
     */
    class DefaultMapSystemContext {
        /**
         * Creates an instance of a MapSystemContext.
         * @param bus This context's event bus.
         * @param projection This context's map projection.
         * @param projectedSize A subscribable which provides the projected size of this context's map.
         * @param deadZone A subscribable which provides the dead zone of this context's map.
         */
        constructor(bus, projection, projectedSize, deadZone) {
            this.bus = bus;
            this.projection = projection;
            this.projectedSize = projectedSize;
            this.deadZone = deadZone;
            /** This context's map model. */
            this.model = new MapModel();
            this.layers = new Map();
            this.controllers = new Map();
        }
        /**
         * Retrieves a layer from this context.
         * @param key The key of the layer to retrieve.
         * @returns The layer in this context with the specified key.
         */
        getLayer(key) {
            return this.layers.get(key);
        }
        /**
         * Retrieves a controller from this context.
         * @param key The key fo the controller to retrieve.
         * @returns The controller in this context with the specified key.
         */
        getController(key) {
            return this.controllers.get(key);
        }
        /**
         * Adds a layer to this context.
         * @param key The key of the layer to add.
         * @param layer The layer to add.
         */
        setLayer(key, layer) {
            this.layers.set(key, layer);
        }
        /**
         * Adds a controller to this context.
         * @param key The key of the controller to add.
         * @param controller The controller to add.
         */
        setController(key, controller) {
            this.controllers.set(key, controller);
        }
    }

    /**
     * A map controller which delegates its behavior to injected callback functions.
     */
    class MapSystemGenericController extends MapSystemController {
        /**
         * Constructor.
         * @param context This controller's map context.
         * @param callbacks The callback functions to which this controller delegates its behavior.
         */
        constructor(context, callbacks) {
            super(context);
            this.callbacks = callbacks;
        }
        /** @inheritdoc */
        onAfterMapRender() {
            this.callbacks.onAfterMapRender && this.callbacks.onAfterMapRender(this.context);
        }
        /** @inheritdoc */
        onDeadZoneChanged(deadZone) {
            this.callbacks.onDeadZoneChanged && this.callbacks.onDeadZoneChanged(this.context, deadZone);
        }
        /** @inheritdoc */
        onMapProjectionChanged(mapProjection, changeFlags) {
            this.callbacks.onMapProjectionChanged && this.callbacks.onMapProjectionChanged(this.context, mapProjection, changeFlags);
        }
        /** @inheritdoc */
        onBeforeUpdated(time, elapsed) {
            this.callbacks.onBeforeUpdated && this.callbacks.onBeforeUpdated(this.context, time, elapsed);
        }
        /** @inheritdoc */
        onAfterUpdated(time, elapsed) {
            this.callbacks.onAfterUpdated && this.callbacks.onAfterUpdated(this.context, time, elapsed);
        }
        /** @inheritdoc */
        onWake() {
            this.callbacks.onWake && this.callbacks.onWake(this.context);
        }
        /** @inheritdoc */
        onSleep() {
            this.callbacks.onSleep && this.callbacks.onSleep(this.context);
        }
        /** @inheritdoc */
        onMapDestroyed() {
            this.callbacks.onMapDestroyed && this.callbacks.onMapDestroyed(this.context);
        }
        /** @inheritdoc */
        destroy() {
            super.destroy();
            this.callbacks.onDestroyed && this.callbacks.onDestroyed(this.context);
        }
    }

    /**
     * A map flight plan renderer that can be supplied styling from the outside.
     */
    class MapSystemPlanRenderer extends AbstractFlightPathPlanRenderer {
        /**
         * Creates an instance of the MapSystemPlanRenderer.
         * @param defaultRoleId The default role ID to render the plan waypoints under.
         * @param renderOrder The order which this renderer renders the flight plan legs. Forward order renders the legs in a first-to-last
         * fashion. Reverse order renders the legs in a last-to-first fashion. Defaults to forward.
         * @param renderActiveLegLast Whether to render the active leg last. Defaults to true.
         */
        constructor(defaultRoleId, renderOrder, renderActiveLegLast) {
            super(renderOrder, renderActiveLegLast);
            this.defaultRoleId = defaultRoleId;
            this.legRenderer = new MapSystemLegRenderer();
            /**
             * A handler that returns a leg rendering style for a given set of leg data.
             * @returns A leg rendering style.
             */
            this.legStyleHandlers = new Map();
            /**
             * A handler that returns whether or not a leg waypoint should be displayed.
             * @returns Whether or not the leg should be displayed.
             */
            this.legWaypointHandlers = new Map();
            /** Whether or not to render flight path ingress turns. */
            this.renderIngress = Subject.create(false);
            /** Whether or not to render flight path egress turns. */
            this.renderEgress = Subject.create(false);
        }
        /** @inheritdoc */
        renderLeg(leg, plan, activeLeg, legIndex, activeLegIndex, context, streamStack) {
            this.legRenderer.currentRenderStyle = FlightPathRenderStyle.Default;
            const handler = this.legStyleHandlers.get(plan.planIndex);
            if (handler !== undefined) {
                this.legRenderer.currentRenderStyle = handler(plan, leg, activeLeg, legIndex, activeLegIndex);
            }
            let partsToRender = exports.FlightPathLegRenderPart.Base
                | (this.renderIngress.get() ? exports.FlightPathLegRenderPart.Ingress : 0)
                | (this.renderEgress.get() ? exports.FlightPathLegRenderPart.Egress : 0);
            if (this.legRenderer.currentRenderStyle.partsToRender !== undefined) {
                partsToRender = this.legRenderer.currentRenderStyle.partsToRender;
            }
            this.legRenderer.render(leg, context, streamStack, partsToRender);
        }
    }
    /**
     * A map system flight plan leg renderer that uses a swappable style.
     */
    class MapSystemLegRenderer extends AbstractFlightPathLegRenderer {
        constructor() {
            super(...arguments);
            this.vectorRenderer = new FlightPathVectorLineRenderer();
            this.currentRenderStyle = new FlightPathRenderStyle();
        }
        /** @inheritdoc */
        renderVector(vector, isIngress, isEgress, leg, context, streamStack) {
            if ('styleBuilder' in this.currentRenderStyle) {
                const currentRenderStyle = this.currentRenderStyle.styleBuilder(vector, isIngress, isEgress);
                this.vectorRenderer.render(vector, context, streamStack, currentRenderStyle.width, currentRenderStyle.style, currentRenderStyle.dash, currentRenderStyle.outlineWidth, currentRenderStyle.outlineStyle, currentRenderStyle.lineCap);
            }
            else {
                if (this.currentRenderStyle.isDisplayed) {
                    this.vectorRenderer.render(vector, context, streamStack, this.currentRenderStyle.width, this.currentRenderStyle.style, this.currentRenderStyle.dash, this.currentRenderStyle.outlineWidth, this.currentRenderStyle.outlineStyle, this.currentRenderStyle.lineCap);
                }
            }
        }
    }
    /**
     * A vector line rendering style to apply to a flight path display on the map.
     */
    class FlightPathRenderStyle {
        /**
         * Creates an instance of a FlightPathRenderStyle.
         * @param isDisplayed Whether or not the path is displayed.
         */
        constructor(isDisplayed = true) {
            this.isDisplayed = isDisplayed;
            /** The pixel width of the path line. */
            this.width = 2;
            /** The style string for the line. */
            this.style = '';
        }
    }
    /** The default rendering style. */
    FlightPathRenderStyle.Default = new FlightPathRenderStyle();
    /** A style that does not display the path. */
    FlightPathRenderStyle.Hidden = new FlightPathRenderStyle(false);

    /**
     * Utility methods related to MapSystem.
     */
    class MapSystemUtils {
        /**
         * Converts a nominal relative projected x coordinate to a true relative projected x coordinate. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param nominalRelX A nominal relative projected x coordinate.
         * @param width The width of the map's projected window, in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @returns The true relative projected x coordinate that is equivalent to the specified nominal coordinate.
         */
        static nominalToTrueRelativeX(nominalRelX, width, deadZone) {
            return (nominalRelX * (width - deadZone[0] - deadZone[2]) + deadZone[0]) / width;
        }
        /**
         * Converts a nominal relative projected y coordinate to a true relative projected y coordinate. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param nominalRelY A nominal relative projected y coordinate.
         * @param height The height of the map's projected window, in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @returns The true relative projected y coordinate that is equivalent to the specified nominal coordinate.
         */
        static nominalToTrueRelativeY(nominalRelY, height, deadZone) {
            return (nominalRelY * (height - deadZone[1] - deadZone[3]) + deadZone[1]) / height;
        }
        /**
         * Converts nominal relative projected coordinates to true relative projected coordinates. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param nominal Nominal relative projected coordinates.
         * @param size The size of the map's projected window, as `[width, height]` in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @param out The vector to which to write the result.
         * @returns The true relative projected coordinates that are equivalent to the specified nominal coordinates.
         */
        static nominalToTrueRelativeXY(nominal, size, deadZone, out) {
            return Vec2Math.set(MapSystemUtils.nominalToTrueRelativeX(nominal[0], size[0], deadZone), MapSystemUtils.nominalToTrueRelativeY(nominal[1], size[1], deadZone), out);
        }
        /**
         * Converts a true relative projected x coordinate to a nominal relative projected x coordinate. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param trueRelX A true relative projected x coordinate.
         * @param width The width of the map's projected window, in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @returns The nominal relative projected x coordinate that is equivalent to the specified true coordinate.
         */
        static trueToNominalRelativeX(trueRelX, width, deadZone) {
            return (trueRelX * width - deadZone[0]) / (width - deadZone[0] - deadZone[2]);
        }
        /**
         * Converts a true relative projected y coordinate to a nominal relative projected y coordinate. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param trueRelY A true relative projected y coordinate.
         * @param height The height of the map's projected window, in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @returns The nominal relative projected y coordinate that is equivalent to the specified true coordinate.
         */
        static trueToNominalRelativeY(trueRelY, height, deadZone) {
            return (trueRelY * height - deadZone[1]) / (height - deadZone[1] - deadZone[3]);
        }
        /**
         * Converts true relative projected coordinates to nominal relative projected coordinates. Nominal relative
         * coordinates are expressed relative to the map's projected width and height, *excluding* dead zones. True relative
         * coordinates are expressed relative to the map's projected width and height, *including* dead zones.
         * @param nominal True relative projected coordinates.
         * @param size The size of the map's projected window, as `[width, height]` in pixels.
         * @param deadZone The map's dead zone, as `[left, top, right, bottom]` in pixels.
         * @param out The vector to which to write the result.
         * @returns The nominal relative projected coordinates that are equivalent to the specified true coordinates.
         */
        static trueToNominalRelativeXY(nominal, size, deadZone, out) {
            return Vec2Math.set(MapSystemUtils.trueToNominalRelativeX(nominal[0], size[0], deadZone), MapSystemUtils.trueToNominalRelativeY(nominal[1], size[1], deadZone), out);
        }
    }

    /**
     * A waypoint renderer for the MapSystem API. Supports addition of string-keyed render roles. Each render role is
     * assigned a position in an ordered list that determines the priority of being chosen when roles are selected for
     * rendering waypoints. For each waypoint, the renderer iterates through all render roles in the priority order list
     * and selects the first role under which the waypoint is registered and is visible.
     */
    class MapSystemWaypointsRenderer extends MapWaypointRenderer {
        /**
         * Constructor.
         * @param textManager The text manager to use for waypoint labels.
         */
        constructor(textManager) {
            super(textManager, (entry, roleDefinitions) => {
                var _a;
                for (let i = 0; i < this.rolePriorityOrder.length; i++) {
                    const role = this.rolePriorityOrder[i];
                    if (entry.isAllRoles(role) && ((_a = roleDefinitions.get(role)) === null || _a === void 0 ? void 0 : _a.visibilityHandler(entry.waypoint))) {
                        return role;
                    }
                }
                return 0;
            });
            this.rolePriorityOrder = [];
            this.rolesByGroup = new Map();
            this.roleIdMap = new Map();
            this.currentBit = 1;
            /** An event that fires when any roles are added. */
            this.onRolesAdded = new SubEvent();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        addRenderRole(arg1, def, group = MapSystemWaypointsRenderer.DefaultGroup) {
            if (typeof arg1 === 'number') {
                return false;
            }
            this.roleIdMap.set(arg1, this.currentBit);
            super.addRenderRole(this.currentBit, def);
            this.rolePriorityOrder.push(this.currentBit);
            let roleGroup = this.rolesByGroup.get(group);
            if (roleGroup === undefined) {
                roleGroup = [];
                this.rolesByGroup.set(group, roleGroup);
            }
            roleGroup.push(arg1);
            this.currentBit *= 2;
            this.onRolesAdded.notify(this);
            return true;
        }
        /**
         * Adds a new named render role to this renderer and inserts it before an existing render role in this renderer's
         * render role selection priority order. Roles positioned earlier in the order have a higher priority for being
         * chosen when roles are selected for rendering waypoints.
         * @param name The name of the render role to add.
         * @param insertBefore The name of the role before which to insert the new role in this renderer's render role
         * selection priority order. If the name does not match any of this renderer's existing render roles, the new role
         * will be placed at the end of the priority order.
         * @param def The render role's definition. If undefined, the new role will be assigned a default definition with
         * no defined rendering context, icon, or label factories, and a visibility handler which always returns true.
         * @param group The group in which to include the new render role, if any. Defaults to
         * {@link MapSystemWaypointsRenderer.DefaultGroup}.
         * @returns Whether the role was successfully inserted.
         */
        insertRenderRole(name, insertBefore, def, group = MapSystemWaypointsRenderer.DefaultGroup) {
            const role = this.currentBit;
            this.addRenderRole(name, def, group);
            const roleToInsertBefore = this.roleIdMap.get(insertBefore);
            if (roleToInsertBefore !== undefined) {
                const indexToInsertBefore = this.rolePriorityOrder.indexOf(roleToInsertBefore);
                if (indexToInsertBefore >= 0 && indexToInsertBefore < this.rolePriorityOrder.length - 1) {
                    this.rolePriorityOrder.pop();
                    this.rolePriorityOrder.splice(indexToInsertBefore, 0, role);
                }
            }
            return true;
        }
        /**
         * Gets a render role associated with a name.
         * @param name The name of the role.
         * @returns The render role associated with the specified name, or undefined if there is no such role.
         */
        getRoleFromName(name) {
            return this.roleIdMap.get(name);
        }
        /**
         * Gets the names of roles in a specified group.
         * @param group A render role group.
         * @returns An array of the names of all render roles belonging to the specified group.
         */
        getRoleNamesByGroup(group) {
            const roleNames = this.rolesByGroup.get(group);
            if (roleNames !== undefined) {
                return roleNames;
            }
            return [];
        }
    }
    /** The default render role group. */
    MapSystemWaypointsRenderer.DefaultGroup = 'DEFAULT_GROUP';
    /**
     * A class that creates icons for the map system waypoint renderer.
     */
    class MapSystemIconFactory {
        constructor() {
            this.cache = new Map();
            this.iconFactories = new Map();
            this.defaultIconFactories = new Map();
        }
        /**
         * Adds an icon factory to the container.
         * @param role The role that this icon factory will be assigned to.
         * @param iconType The unique string type name of the icon.
         * @param factory The factory that will produce the icon.
         */
        addIconFactory(role, iconType, factory) {
            if (!this.iconFactories.has(role)) {
                this.iconFactories.set(role, new Map());
            }
            const roleFactories = this.iconFactories.get(role);
            roleFactories.set(iconType, factory);
        }
        /**
         * Adds a default icon factory for a role.
         * @param role The role to add a default icon factory for.
         * @param factory The factory that will produce the icons.
         */
        addDefaultIconFactory(role, factory) {
            this.defaultIconFactories.set(role, factory);
        }
        /** @inheritdoc */
        getIcon(role, waypoint) {
            if (!this.cache.has(role)) {
                this.cache.set(role, new Map());
            }
            const roleCache = this.cache.get(role);
            let icon = roleCache.get(waypoint.uid);
            if (icon === undefined) {
                icon = this.createIcon(role, waypoint);
                roleCache.set(waypoint.uid, icon);
            }
            return icon;
        }
        /**
         * Creates a new icon for a waypoint.
         * @param role The role that has been selected to render.
         * @param waypoint The waypoint for which to create an icon.
         * @returns a waypoint icon.
         */
        createIcon(role, waypoint) {
            if (!this.iconFactories.has(role)) {
                this.iconFactories.set(role, new Map());
            }
            const roleFactories = this.iconFactories.get(role);
            const factory = roleFactories.get(waypoint.type);
            if (factory !== undefined) {
                return factory(waypoint);
            }
            else {
                const defaultFactory = this.defaultIconFactories.get(role);
                if (defaultFactory !== undefined) {
                    return defaultFactory(waypoint);
                }
            }
            const imageEl = document.createElement('img');
            imageEl.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFjSURBVFhHvZarTsVAEIa3qCMQCAxPgEEikDwGAkGCAIFAIHgQHgGJ4C0Q4AiBN8CQQAIJuOGfs9N0d3p62tlLv+RPd2fFNrNfLy4Lov1lMtiQayqnyJkfzg3RAvlEfpEtqZrJ6cARwhsvkGMuzAvRA0KSZ6nOBNFesHmbA1k1kXoEq8SbScZOPt2BJBlTOtDKx7whT344l4yxfFfIeTCvLGMsH7d8G9lEvqXGMcloPYJQtHvXNB/ID8a3vrSkkox9+Q5lhdf4m9DWs96MwxCdBJu8SrWD6DFYv5BqQbR8mqoyrpJPkyjjVAn78mmqybhOPk0VGcfk0xSXcUw+TVEZp8inMco4JuG4fJpiMlrk0xSR0SqfJltGq3yaLBlT5NNMlHFIQrt8mokyNnLtYPmce0dace6QFz80s4vwLxzzh+zgxr78dIhYvtK5lF3WEMtXOr2nKT4C3/5rP6nGTZJTdXDuH4TJQyPZ/x+gAAAAAElFTkSuQmCC';
            return new MapWaypointImageIcon(waypoint, 0, imageEl, Vec2Math.create(24, 24));
        }
    }
    /**
     * A class that create labels for the map system waypoint renderer.
     */
    class MapSystemLabelFactory {
        constructor() {
            this.cache = new Map();
            this.labelFactories = new Map();
            this.defaultLabelFactories = new Map();
        }
        /**
         * Adds an label factory to the container.
         * @param role The role to add this label factory for.
         * @param iconType The unique string type name of the waypoint.
         * @param factory The factory that will produce the waypoint label.
         */
        addLabelFactory(role, iconType, factory) {
            if (!this.labelFactories.has(role)) {
                this.labelFactories.set(role, new Map());
            }
            const roleFactories = this.labelFactories.get(role);
            roleFactories.set(iconType, factory);
        }
        /**
         * Adds a default label factory for a role.
         * @param role The role to add a default label factory for.
         * @param factory The factory that will produce the labels.
         */
        addDefaultLabelFactory(role, factory) {
            this.defaultLabelFactories.set(role, factory);
        }
        /** @inheritdoc */
        getLabel(role, waypoint) {
            if (!this.cache.has(role)) {
                this.cache.set(role, new Map());
            }
            const roleCache = this.cache.get(role);
            let label = roleCache.get(waypoint.uid);
            if (label === undefined) {
                label = this.createLabel(role, waypoint);
                roleCache.set(waypoint.uid, label);
            }
            return label;
        }
        /**
         * Creates a new label for a waypoint.
         * @param role The role that has been selected to render.
         * @param waypoint The waypoint to create a label for.
         * @returns A new waypoint label.
         */
        createLabel(role, waypoint) {
            if (!this.labelFactories.has(role)) {
                this.labelFactories.set(role, new Map());
            }
            const roleFactories = this.labelFactories.get(role);
            const factory = roleFactories.get(waypoint.type);
            if (factory !== undefined) {
                return factory(waypoint);
            }
            else {
                const defaultFactory = this.defaultLabelFactories.get(role);
                if (defaultFactory !== undefined) {
                    return defaultFactory(waypoint);
                }
            }
            let text = '';
            if (FacilityWaypointUtils.isFacilityWaypoint(waypoint)) {
                text = ICAO.getIdent(waypoint.facility.get().icao);
            }
            return new MapCullableLocationTextLabel(text, 0, waypoint.location, false, { fontSize: 22, font: 'monospace', anchor: new Float64Array([-0.25, 0.4]) });
        }
    }

    /**
     * Builds maps. Each builder is configured with a series of build steps which collectively define how the builder
     * compiles finished maps. In addition to defining basic map properties such as size and range, build steps can also
     * customize map behavior and appearance through adding map model modules, layers, and controllers.
     *
     * Each map compiled by the builder is associated with a {@link MapSystemContext}, which holds references to the map
     * projection, map model, all layers and controllers, and other data associated with the map. Layers and controllers
     * have access to the context when they are created during compilation, and a reference to the context is stored with
     * the compiled map.
     *
     * A single builder can compile multiple maps. Each compiled map is a separate entity, with its own model, layers,
     * controllers, and context.
     */
    class MapSystemBuilder {
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of map model modules added to this builder. */
        get moduleCount() {
            return this.moduleFactories.size;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of map layers added to this builder. */
        get layerCount() {
            return this.layerFactories.size;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The number of map controllers added to this builder. */
        get controllerCount() {
            return this.controllerFactories.size;
        }
        /**
         * Creates an instance of a map system builder.
         * @param bus This builder's event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.moduleFactories = new Map();
            this.layerFactories = new Map();
            this.controllerFactories = new Map();
            this.contextFactories = new Map();
            this.initCallbacks = new Map();
            this.destroyCallbacks = new Map();
            this.projectedSize = Subject.create(Vec2Math.create(100, 100));
        }
        /**
         * Creates a new Garmin map builder. The builder is initialized with a default projected size of `[100, 100]` pixels.
         * @param bus The event bus.
         * @returns A new Garmin map builder.
         */
        static create(bus) {
            return new MapSystemBuilder(bus);
        }
        /**
         * Configures this builder to generate a map with a given projected window size.
         * @param size The size of the projected window, as `[width, height]` in pixels, or a subscribable which provides it.
         * @returns This builder, after it has been configured.
         */
        withProjectedSize(size) {
            this.projectedSize = 'isSubscribable' in size ? size : Subject.create(size);
            return this;
        }
        /**
         * Configures this builder to generate a map with a given dead zone.
         * @param deadZone The dead zone, as `[left, top, right, bottom]` in pixels, or a subscribable which provides it.
         * @returns This builder, after it has been configured.
         */
        withDeadZone(deadZone) {
            this.deadZone = 'isSubscribable' in deadZone ? deadZone : VecNSubject.createFromVector(new Float64Array(deadZone));
            return this;
        }
        /**
         * Configures this builder to generate a map with an initial projected target offset.
         * @param offset The initial projected target offset, as `[x, y]` in pixels.
         * @returns This builder, after it has been configured.
         */
        withTargetOffset(offset) {
            this.targetOffset = offset;
            return this;
        }
        /**
         * Configures this builder to generate a map with specific initial range endpoints. The endpoints are defined
         * relative to the width and height of the map's projected window, *excluding* the dead zone.
         * @param endpoints The initial range endpoints, as `[x1, y1, x2, y2]`.
         * @returns This builder, after it has been configured.
         */
        withRangeEndpoints(endpoints) {
            this.nominalRangeEndpoints = endpoints;
            return this;
        }
        /**
         * Configures this build to generate a map with a specific initial range.
         * @param range The initial range.
         * @returns This builder, after it has been configured.
         */
        withRange(range) {
            this.range = range.asUnit(UnitType.GA_RADIAN);
            return this;
        }
        /**
         * Adds a map module to this builder. When this builder compiles its map, all added modules will be created and added
         * to the map's model. If an existing module has been added to this builder with the same key, it will be replaced.
         * @param key The key (name) of the module.
         * @param factory A function which creates the module.
         * @returns This builder, after the map module has been added.
         */
        withModule(key, factory) {
            this.moduleFactories.set(key, { key, factory });
            return this;
        }
        /**
         * Adds a map layer to this builder. When this builder compiles its map, all added layers will be created and
         * attached to the map. Layers with a lower assigned order will be attached before and appear below layers with
         * greater assigned order values. If an existing layer has been added to this builder with the same key, it will be
         * replaced.
         * @param key The key of the layer.
         * @param factory A function which renders the layer as a VNode.
         * @param order The order assigned to the layer. Layers with lower assigned order will be attached to the map before
         * and appear below layers with greater assigned order values. Defaults to the number of layers already added to this
         * builder.
         * @returns This builder, after the map layer has been added, or `never` if this builder does not have all the
         * modules required by the layer.
         */
        withLayer(key, factory, order) {
            // Delete the key to ensure a consistent layer order.
            const wasDeleted = this.layerFactories.delete(key);
            this.layerFactories.set(key, { key, factory, order: order !== null && order !== void 0 ? order : (this.layerFactories.size + (wasDeleted ? 1 : 0)) });
            return this;
        }
        /**
         * Adds a controller to this builder. When this builder compiles its map, all added controllers will be created and
         * hooked up to the map's lifecycle callbacks. If an existing controller has been added to this builder with the same
         * key, it will be replaced.
         * @param key The key of the controller.
         * @param factory A function which creates the controller.
         * @returns This builder, after the map layer has been added, or `never` if this builder does not have all the
         * modules required by the controller.
         */
        withController(key, factory) {
            this.controllerFactories.set(key, { factory });
            return this;
        }
        /**
         * Adds a context property to this builder. When the builder compiles its map, all added properties will be available
         * on the context. Properties are created on the context in the order they were added to the builder, and property
         * factories have access to previously created properties on the context. If an existing property has been added to
         * this builder with the same key, it will be replaced.
         * @param key The key of the property to add.
         * @param factory A function which creates the value of the property.
         * @returns This builder, after the context property has been added.
         */
        withContext(key, factory) {
            var _a;
            if (!MapSystemBuilder.RESTRICTED_CONTEXT_KEYS.has(key)) {
                const existing = this.contextFactories.get(key);
                const order = (_a = existing === null || existing === void 0 ? void 0 : existing.order) !== null && _a !== void 0 ? _a : this.contextFactories.size;
                this.contextFactories.set(key, { key, factory, order });
            }
            return this;
        }
        /**
         * Configures this builder to execute a callback function immediately after it is finished compiling a map. If an
         * existing callback has been added to this builder with the same key, it will be replaced.
         * @param key The key of the callback.
         * @param callback The callback function to add.
         * @returns This builder, after the callback has been added.
         */
        withInit(key, callback) {
            this.initCallbacks.set(key, callback);
            return this;
        }
        /**
         * Configures this builder to execute a callback function after a built map is destroyed. If an existing callback has
         * been added to this builder with the same key, it will be replaced.
         * @param key The key of the callback.
         * @param callback The callback function to add.
         * @returns This builder, after the callback has been added.
         */
        withDestroy(key, callback) {
            this.destroyCallbacks.set(key, callback);
            return this;
        }
        /**
         * Assigns an order value to a layer. Layers with a lower assigned order will be attached before and appear below
         * layers with greater assigned order values.
         * @param key The key of the layer to which to assign the order value.
         * @param order The order value to assign.
         * @returns This builder, after the order value has been assigned.
         */
        withLayerOrder(key, order) {
            const factory = this.layerFactories.get(key);
            if (factory) {
                // Delete the key to ensure a consistent layer order.
                this.layerFactories.delete(key);
                factory.order = order;
                this.layerFactories.set(key, factory);
            }
            return this;
        }
        /**
         * Configures this builder to add a controller which maintains a list of bindings from source to target
         * subscribables.
         * @param key The key of the controller.
         * @param bindings The bindings to maintain.
         * @param onDestroy A function to execute when the controller is destroyed.
         * @returns This builder, after it has been configured.
         */
        withBindings(key, bindings, onDestroy) {
            return this.withController(key, context => new MapBindingsController(context, bindings(context), onDestroy));
        }
        /**
         * Configures this builder to generate a map which is updated at a regular frequency based on event bus clock events.
         *
         * Adds the following...
         *
         * Context properties:
         * * `'updateFreq': Subscribable<number>`
         *
         * Controllers:
         * * `[MapSystemKeys.ClockUpdate]: MapClockUpdateController`.
         * @param updateFreq The map's update frequency, in hertz, or a subscribable which provides it.
         * @returns This builder, after it has been configured.
         */
        withClockUpdate(updateFreq) {
            return this
                .withContext('updateFreq', () => typeof updateFreq === 'number' ? Subject.create(updateFreq) : updateFreq)
                .withController(MapSystemKeys.ClockUpdate, context => new MapClockUpdateController(context));
        }
        /**
         * Configures this builder to add a resource moderator for control of the map's projection target.
         *
         * Adds the context property `[MapSystemKeys.TargetControl]: ResourceModerator<void>`.
         * @returns This builder, after the resource moderator has been added.
         */
        withTargetControlModerator() {
            return this.withContext(MapSystemKeys.TargetControl, () => new ResourceModerator(undefined));
        }
        /**
         * Configures this builder to add a resource moderator for control of the map's rotation.
         *
         * Adds the context property `[MapSystemKeys.RotationControl]: ResourceModerator<void>`.
         * @returns This builder, after the resource moderator has been added.
         */
        withRotationControlModerator() {
            return this.withContext(MapSystemKeys.RotationControl, () => new ResourceModerator(undefined));
        }
        /**
         * Configures this builder to add a resource moderator for control of the map's range.
         *
         * Adds the context property `[MapSystemKeys.RangeControl]: ResourceModerator<void>`.
         * @returns This builder, after the resource moderator has been added.
         */
        withRangeControlModerator() {
            return this.withContext(MapSystemKeys.RangeControl, () => new ResourceModerator(undefined));
        }
        /**
         * Configures this builder to generate a map whose projection target follows the player airplane. The follow airplane
         * behavior will be active if and only if the controller owns the projection target control resource. The
         * controller's priority for the resource is `0`.
         *
         * Adds the following...
         *
         * Context properties:
         * * `[MapSystemKeys.TargetControl]: ResourceModerator<void>`
         *
         * Modules:
         * * `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsModule`
         * * `[MapSystemKeys.FollowAirplane]: MapFollowAirplaneModule`
         *
         * Controllers:
         * * `[MapSystemKeys.FollowAirplane]: MapFollowAirplaneController`
         * @returns This builder, after it has been configured.
         */
        withFollowAirplane() {
            return this
                .withModule(MapSystemKeys.OwnAirplaneProps, () => new MapOwnAirplanePropsModule)
                .withModule(MapSystemKeys.FollowAirplane, () => new MapFollowAirplaneModule())
                .withTargetControlModerator()
                .withController(MapSystemKeys.FollowAirplane, context => new MapFollowAirplaneController(context));
        }
        /**
         * Configures this builder to generate a map which supports common rotation behavior. The rotation behavior will be
         * active if and only if the controller owns the rotation control resource. The controller's priority for the
         * resource is `0`.
         *
         * Requires the module `'ownAirplaneProps': MapOwnAirplanePropsModule` to support player airplane-derived rotation
         * behavior, such as Heading Up and Track Up.
         *
         * Adds the following...
         *
         * Context properties:
         * * `'[MapSystemKeys.RotationControl]': ResourceModerator<void>`
         *
         * Modules:
         * * `[MapSystemKeys.Rotation]: MapRotationModule`
         *
         * Controllers:
         * * `[MapSystemKeys.Rotation]: MapRotationController`
         * @returns This builder, after it has been configured.
         */
        withRotation() {
            return this
                .withModule(MapSystemKeys.Rotation, () => new MapRotationModule())
                .withRotationControlModerator()
                .withController(MapSystemKeys.Rotation, context => new MapRotationController(context));
        }
        /**
         * Configures this builder to generate a map which displays an icon depicting the position of the player airplane.
         *
         * Adds the following...
         *
         * Modules:
         * * `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsModule`
         * * `[MapSystemKeys.OwnAirplaneIcon]: MapOwnAirplaneIconModule`
         *
         * Layers:
         * * `[MapSystemKeys.OwnAirplaneIcon]: MapOwnAirplaneLayer`
         * @param iconSize The size of the icon, in pixels.
         * @param iconFilePath The path to the icon's image asset, or a subscribable which provides it.
         * @param iconAnchor The point on the icon that is anchored to the airplane's position, or a subscribable which
         * provides it. The point is expressed as a 2-tuple relative to the icon's width and height, with `[0, 0]` at the
         * top left and `[1, 1]` at the bottom right.
         * @param cssClass The CSS class(es) to apply to the root of the airplane icon layer.
         * @param order The order assigned to the icon layer. Layers with lower assigned order will be attached to the map
         * before and appear below layers with greater assigned order values. Defaults to the number of layers already added
         * to this builder.
         * @returns This builder, after it has been configured.
         */
        withOwnAirplaneIcon(iconSize, iconFilePath, iconAnchor, cssClass, order) {
            return this
                .withModule(MapSystemKeys.OwnAirplaneProps, () => new MapOwnAirplanePropsModule())
                .withModule(MapSystemKeys.OwnAirplaneIcon, () => new MapOwnAirplaneIconModule())
                .withLayer(MapSystemKeys.OwnAirplaneIcon, (context) => {
                return (exports.FSComponent.buildComponent(MapOwnAirplaneLayer, { model: context.model, mapProjection: context.projection, imageFilePath: iconFilePath, iconSize: iconSize, iconAnchor: iconAnchor, class: cssClass }));
            }, order);
        }
        /**
         * Configures this builder to add a controller which controls and optimizes the orientation of the own airplane icon
         * in response to a desired orientation and the map rotation type. If the desired orientation matches the map
         * rotation (e.g. both Heading Up), the icon orientation is set to Map Up; otherwise the orientation is set to the
         * desired orientation.
         *
         * Requires the modules `[MapSystemKeys.OwnAirplaneIcon]: MapOwnAirplaneIconModule` and
         * `[MapSystemKeys.Rotation]: MapRotationModule`.
         *
         * Adds the controller `[MapSystemKeys.OwnAirplaneIconOrientation]: MapOwnAirplaneIconOrientationController`.
         * @param desiredOrientation The desired orientation of the own airplane icon.
         * @returns This builder, after it has been configured.
         */
        withOwnAirplaneIconOrientation(desiredOrientation) {
            return this.withController(MapSystemKeys.OwnAirplaneIconOrientation, context => new MapOwnAirplaneIconOrientationController(context, desiredOrientation));
        }
        /**
         * Configures this builder to bind properties in an added {@link MapOwnAirplanePropsModule} to data derived from
         * event bus events.
         *
         * Requires the module `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsModule`.
         *
         * Adds the controller `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsController`.
         * @param properties The properties to bind.
         * @param updateFreq The update frequency, in hertz, or a subscribable which provides it.
         * @returns This builder, after it has been configured.
         */
        withOwnAirplanePropBindings(properties, updateFreq) {
            return this.withController(MapSystemKeys.OwnAirplaneProps, context => new MapOwnAirplanePropsController(context, properties, typeof updateFreq === 'number' ? Subject.create(updateFreq) : updateFreq));
        }
        /**
         * Configures this builder to add a module describing the player airplane's autopilot properties, and optionally
         * binds the module's properties to data received over the event bus.
         *
         * Adds the following...
         *
         * Modules:
         * * `[MapSystemKeys.AutopilotProps]: MapAutopilotPropsModule`
         *
         * Controllers:
         * * `[MapSystemKeys.AutopilotProps]: MapAutopilotPropsController` (optional)
         * @param propertiesToBind Properties on the autopilot module to bind to data received over the event bus.
         * @param updateFreq The update frequency, in hertz, of the data bindings, or a subscribable which provides it. If
         * not defined, the data bindings will update every frame. Ignored if `propertiesToBind` is undefined.
         * @returns This builder, after it has been configured.
         */
        withAutopilotProps(propertiesToBind, updateFreq) {
            this.withModule(MapSystemKeys.AutopilotProps, () => new MapAutopilotPropsModule());
            if (propertiesToBind !== undefined) {
                this.withController(MapSystemKeys.AutopilotProps, context => new MapAutopilotPropsController(context, propertiesToBind, updateFreq));
            }
            return this;
        }
        /**
         * Configures this builder to generate a map which includes a layer displaying text.
         *
         * Adds the following...
         *
         * Context properties:
         * * `[MapSystemKeys.TextManager]: MapCullableTextLabelManager`
         *
         * Layers:
         * * `[MapSystemKeys.TextLayer]: MapCullableTextLayer`
         * @param enableCulling Whether to enable text culling. Defaults to `false`.
         * @param order The order value to assign to the text layer. Layers with lower assigned order will be attached to
         * the map before and appear below layers with greater assigned order values. Defaults to the number of layers
         * already added to this builder.
         * @param cssClass The CSS class(es) to apply to the text layer.
         * @returns This builder, after it has been configured.
         */
        withTextLayer(enableCulling, order, cssClass) {
            return this.withContext(MapSystemKeys.TextManager, () => new MapCullableTextLabelManager(enableCulling))
                .withLayer(MapSystemKeys.TextLayer, (context) => {
                return (exports.FSComponent.buildComponent(MapCullableTextLayer, { model: context.model, mapProjection: context.projection, manager: context.textManager, class: cssClass }));
            }, order);
        }
        /**
         * Configures this builder to generate a map which displays Bing Map terrain and weather.
         *
         * Adds the following...
         *
         * Modules:
         * * `[MapSystemKeys.TerrainColors]: MapTerrainColorsModule`
         * * `[MapSystemKeys.Weather]: MapWxrModule`
         *
         * Layers:
         * * `[MapSystemKeys.Bing]: MapBingLayer`
         * @param bingId The ID to assign to the Bing Map instance bound to the layer.
         * @param options Options with which to configure the layer.
         * @param order The order value to assign to the Bing layer. Layers with lower assigned order will be attached to the
         * map before and appear below layers with greater assigned order values. Defaults to the number of layers already
         * added to the map builder.
         * @param cssClass The CSS class(es) to apply to the flight plan canvas elements.
         * @returns This builder, after it has been configured.
         */
        withBing(bingId, options, order, cssClass) {
            return this
                .withModule(MapSystemKeys.TerrainColors, () => new MapTerrainColorsModule())
                .withModule(MapSystemKeys.Weather, () => new MapWxrModule())
                .withLayer(MapSystemKeys.Bing, context => {
                const terrainColors = context.model.getModule('terrainColors');
                const weather = context.model.getModule('weather');
                return (exports.FSComponent.buildComponent(MapBingLayer, { model: context.model, mapProjection: context.projection, bingId: bingId, bingDelay: options === null || options === void 0 ? void 0 : options.bingDelay, bingSkipUnbindOnDestroy: options === null || options === void 0 ? void 0 : options.bingSkipUnbindOnDestroy, reference: terrainColors.reference, earthColors: terrainColors.colors, earthColorsElevationRange: terrainColors.colorsElevationRange, isoLines: terrainColors.showIsoLines, wxrMode: weather.wxrMode, wxrColors: weather.weatherRadarColors, opacity: options === null || options === void 0 ? void 0 : options.opacity, class: cssClass }));
            }, order);
        }
        /**
         * Configures this builder to generate a map which uses a {@link MapSystemWaypointsRenderer} to render waypoints.
         *
         * Requires the `[MapSystemKeys.TextManager]: MapCullableTextLabelManager` context property.
         *
         * Adds the `[MapSystemKeys.WaypointRenderer]: MapSystemWaypointsRenderer` context property.
         * @returns This builder, after it has been configured.
         */
        withWaypoints() {
            return this
                .withContext(MapSystemKeys.WaypointRenderer, context => new MapSystemWaypointsRenderer(context[MapSystemKeys.TextManager]))
                .withController('waypointRendererUpdate', context => new MapSystemGenericController(context, {
                onAfterUpdated: (contextArg) => { contextArg[MapSystemKeys.WaypointRenderer].update(context.projection); }
            }));
        }
        /**
         * Configures this builder to generate a map which displays waypoints near the map center or target. Waypoints
         * displayed in this manner are rendered by a {@link MapSystemWaypointsRenderer}.
         *
         * If a text layer has already been added to the builder, its order will be changed so that it is rendered above the
         * waypoint layer. Otherwise, a text layer will be added to the builder after the waypoint layer.
         *
         * Adds the following...
         *
         * Context properties:
         * * `[MapSystemKeys.TextManager]: MapCullableTextLabelManager`
         * * `[MapSystemKeys.IconFactory]: MapSystemIconFactory`
         * * `[MapSystemKeys.LabelFactory]: MapSystemLabelFactory`
         *
         * Modules:
         * * `[MapSystemKeys.NearestWaypoints]: MapWaypointDisplayModule`
         *
         * Layers:
         * * `[MapSystemKeys.NearestWaypoints]: MapSystemWaypointsLayer`
         * * `[MapSystemKeys.TextLayer]: MapCullableTextLayer`
         * @param configure A function to configure the waypoint display.
         * @param enableTextCulling Whether to enable text culling on the text manager.
         * @param order The order to assign to the waypoint layer. Layers with lower assigned order will be attached to the
         * map before and appear below layers with greater assigned order values. Defaults to the number of layers already
         * added to this builder.
         * @param cssClass The CSS class(es) to apply to the root of the nearest waypoints layer.
         * @returns This builder, after it has been configured.
         */
        withNearestWaypoints(configure, enableTextCulling = false, order, cssClass) {
            this
                .withTextLayer(enableTextCulling)
                .withModule(MapSystemKeys.NearestWaypoints, () => new MapWaypointDisplayModule())
                .withWaypoints()
                .withContext(MapSystemKeys.IconFactory, () => new MapSystemIconFactory())
                .withContext(MapSystemKeys.LabelFactory, () => new MapSystemLabelFactory());
            let facilityWaypointCache = undefined;
            this.withContext('useTargetAsWaypointSearchCenter', context => {
                context[MapSystemKeys.WaypointRenderer].addRenderRole(exports.MapSystemWaypointRoles.Normal, undefined, exports.MapSystemWaypointRoles.Normal);
                const builder = new WaypointDisplayBuilder(context[MapSystemKeys.IconFactory], context[MapSystemKeys.LabelFactory], context[MapSystemKeys.WaypointRenderer]);
                configure(builder);
                facilityWaypointCache = builder.getWaypointCache();
                return builder.getIsCenterTarget();
            });
            const layerCount = this.layerCount;
            return this
                .withLayer(MapSystemKeys.NearestWaypoints, context => {
                return (exports.FSComponent.buildComponent(MapSystemWaypointsLayer, { bus: context.bus, waypointRenderer: context[MapSystemKeys.WaypointRenderer], model: context.model, mapProjection: context.projection, iconFactory: context[MapSystemKeys.IconFactory], labelFactory: context[MapSystemKeys.LabelFactory], useMapTargetAsSearchCenter: context.useTargetAsWaypointSearchCenter, waypointCache: facilityWaypointCache, class: cssClass }));
            }, order)
                .withLayerOrder(MapSystemKeys.TextLayer, order !== null && order !== void 0 ? order : layerCount);
        }
        /**
         * Configures this builder to generate a map which displays a flight plan. Waypoints displayed as part of the flight
         * plan are rendered by a {@link MapSystemWaypointsRenderer}.
         *
         * If a text layer has already been added to the builder, its order will be changed so that it is rendered above the
         * waypoint layer. Otherwise, a text layer will be added to the builder after the waypoint layer.
         *
         * Adds the following...
         *
         * Context properties:
         * * `[MapSystemKeys.FlightPlanner]: FlightPlanner`
         * * `[MapSystemKeys.TextManager]: MapCullableTextLabelManager`
         * * `[MapSystemKeys.IconFactory]: MapSystemIconFactory`
         * * `[MapSystemKeys.LabelFactory]: MapSystemLabelFactory`
         * * `[MapSystemKeys.FlightPathRenderer]: MapSystemPlanRenderer`
         *
         * Modules:
         * * `[MapSystemKeys.FlightPlan]: MapFlightPlanModule`
         *
         * Layers:
         * * `` `${[MapSystemKeys.FlightPlan]}${planIndex}`: MapSystemFlightPlanLayer ``
         * * `[MapSystemKeys.TextLayer]: MapCullableTextLayer`
         *
         * Controllers:
         * * `[MapSystemKeys.FlightPlan]: MapFlightPlanController`
         * @param configure A function to configure the waypoint display.
         * @param flightPlanner The flight planner.
         * @param planIndex The index of the flight plan to display.
         * @param enableTextCulling Whether to enable text culling on the text manager.
         * @param order The order to assign to the plan layer. Layers with lower assigned order will be attached to the
         * map before and appear below layers with greater assigned order values. Defaults to the number of layers already
         * added to this builder.
         * @param cssClass The CSS class(es) to apply to the flight plan canvas elements.
         * @returns This builder, after it has been configured.
         */
        withFlightPlan(configure, flightPlanner, planIndex, enableTextCulling = false, order, cssClass) {
            this
                .withTextLayer(enableTextCulling, undefined, 'flight-plan-text-layer')
                .withModule(MapSystemKeys.FlightPlan, () => new MapFlightPlanModule())
                .withWaypoints()
                .withContext(MapSystemKeys.FlightPlanner, () => flightPlanner)
                .withContext(MapSystemKeys.IconFactory, () => new MapSystemIconFactory())
                .withContext(MapSystemKeys.LabelFactory, () => new MapSystemLabelFactory())
                .withContext(MapSystemKeys.FlightPathRenderer, () => new MapSystemPlanRenderer(1))
                .withController(MapSystemKeys.FlightPlan, context => new MapFlightPlanController(context))
                .withInit(`${MapSystemKeys.FlightPlan}${planIndex}`, context => {
                const builder = new FlightPlanDisplayBuilder(context[MapSystemKeys.IconFactory], context[MapSystemKeys.LabelFactory], context[MapSystemKeys.WaypointRenderer], context[MapSystemKeys.FlightPathRenderer], planIndex);
                context[MapSystemKeys.WaypointRenderer].insertRenderRole(exports.MapSystemWaypointRoles.FlightPlan, exports.MapSystemWaypointRoles.Normal, undefined, `${exports.MapSystemWaypointRoles.FlightPlan}_${planIndex}`);
                configure(builder, context);
            });
            const layerCount = this.layerCount;
            return this
                .withLayer(`${MapSystemKeys.FlightPlan}${planIndex}`, (context) => {
                return (exports.FSComponent.buildComponent(MapSystemFlightPlanLayer, { bus: context.bus, waypointRenderer: context[MapSystemKeys.WaypointRenderer], model: context.model, mapProjection: context.projection, iconFactory: context[MapSystemKeys.IconFactory], labelFactory: context[MapSystemKeys.LabelFactory], flightPathRenderer: context[MapSystemKeys.FlightPathRenderer], planIndex: planIndex, class: cssClass }));
            }, order)
                .withLayerOrder(MapSystemKeys.TextLayer, order !== null && order !== void 0 ? order : layerCount);
        }
        /**
         * Configures this builder to generate a map which displays airspaces.
         *
         * Adds the following...
         *
         * Context properties:
         * * `[MapSystemKeys.AirspaceManager]: GenericAirspaceRenderManager`
         *
         * Modules:
         * * `[MapSystemKeys.Airspace]: MapAirspaceModule`
         *
         * Layers:
         * * `[MapSystemKeys.Airspace]: MapAirspaceLayer`
         * @param cache The airspace cache to use to store airspaces retrieved for rendering.
         * @param showTypes The airspace show types to define in the airspace module. Each show type will be assigned a
         * {@link Subject} in the `show` property of the module. The Subject controls the visibility of airspace types
         * included in its show type. Airspace types that are not included in any defined show type will never be displayed.
         * @param selectRenderer A function which selects a {@link MapAirspaceRenderer}
         * @param renderOrder A function which determines the rendering order of airspaces. The function should return a
         * negative number when airspace `a` should be rendered before (below) airspace `b`, a positive number when airspace
         * `a` should be rendered after (above) airspace `b`, and `0` when the relative render order of the two airspaces
         * does not matter. If not defined, there will be no guarantee on the order in which airspaces are rendered.
         * @param options Options for the airspace layer. Option defaults are as follows:
         * * `maxSearchRadius`: 10 nautical miles
         * * `maxSearchItemCount`: 100
         * * `searchDebounceDelay`: 500 (milliseconds)
         * * `renderTimeBudget`: 0.2 (milliseconds)
         * @param order The order to assign to the airspace layer. Layers with lower assigned order will be attached to the
         * map before and appear below layers with greater assigned order values. Defaults to the number of layers already
         * added to this builder.
         * @returns This builder, after it has been configured.
         */
        withAirspaces(cache, showTypes, selectRenderer, renderOrder = () => 0, options, order) {
            return this
                .withModule(MapSystemKeys.Airspace, () => new MapAirspaceModule(showTypes))
                .withContext(MapSystemKeys.AirspaceManager, () => new GenericAirspaceRenderManager(renderOrder, selectRenderer))
                .withLayer(MapSystemKeys.Airspace, context => {
                var _a, _b;
                const optionsToUse = Object.assign({}, options);
                (_a = optionsToUse.maxSearchRadius) !== null && _a !== void 0 ? _a : (optionsToUse.maxSearchRadius = Subject.create(UnitType.NMILE.createNumber(10)));
                (_b = optionsToUse.maxSearchItemCount) !== null && _b !== void 0 ? _b : (optionsToUse.maxSearchItemCount = Subject.create(100));
                return (exports.FSComponent.buildComponent(MapAirspaceLayer, Object.assign({ model: context.model, mapProjection: context.projection, bus: context.bus, lodBoundaryCache: cache, airspaceRenderManager: context[MapSystemKeys.AirspaceManager] }, optionsToUse)));
            }, order);
        }
        /**
         * Configures this builder to generate a map which displays TCAS intruders.
         *
         * Adds the following...
         *
         * Modules:
         * * `[MapSystemKeys.OwnAirplaneProps]: MapOwnAirplanePropsModule`
         * * `[MapSystemKeys.Traffic]: MapTrafficModule`
         *
         * Layers:
         * * `[MapSystemKeys.Traffic]: MapSystemTrafficLayer`
         * @param tcas The TCAS used by the traffic display.
         * @param iconFactory A function which creates intruder icons for the traffic display.
         * @param initCanvasStyles A function which initializes global canvas styles for the traffic display.
         * @param offScaleOobOptions A function which generates options for handling off-scale and out-of-bounds intruders.
         * @param order The order to assign to the traffic layer. Layers with lower assigned order will be attached to the
         * map before and appear below layers with greater assigned order values. Defaults to the number of layers already
         * added to this builder.
         * @param cssClass The CSS class(es) to apply to the root of the traffic component.
         * @returns This builder, after it has been configured.
         */
        withTraffic(tcas, iconFactory, initCanvasStyles, offScaleOobOptions, order, cssClass) {
            return this
                .withModule(MapSystemKeys.OwnAirplaneProps, () => new MapOwnAirplanePropsModule())
                .withModule(MapSystemKeys.Traffic, () => new MapTrafficModule(tcas))
                .withLayer(MapSystemKeys.Traffic, context => {
                const options = offScaleOobOptions !== undefined ? Object.assign({}, offScaleOobOptions(context)) : {};
                if (options.oobOffset !== undefined && !('isSubscribable' in options.oobOffset)) {
                    options.oobOffset = Subject.create(options.oobOffset);
                }
                return (exports.FSComponent.buildComponent(MapSystemTrafficLayer, Object.assign({ context: context, model: context.model, mapProjection: context.projection, iconFactory: iconFactory, initCanvasStyles: initCanvasStyles, class: cssClass }, options)));
            }, order);
        }
        /**
         * Configures this builder using a custom build step.
         * @param builder A function which defines a custom build step.
         * @param args Arguments to pass to the custom build function.
         * @returns This builder, after it has been configured.
         */
        with(builder, ...args) {
            return builder(this, ...args);
        }
        /**
         * Compiles a map. The compiled map consists of a map context, a rendered map (as a VNode), and a node reference to
         * the rendered map component.
         *
         * The compiled map will be bound to a model (accessible through the map context) which contains all the modules
         * added to this builder.
         *
         * The map will also contain all layers added to this builder, with layers assigned lower order values appearing
         * below layers assigned greater order values. The layers can be retrieved by their keys from the map context.
         *
         * All controllers added to this builder will be created with the map and hooked up to the map's lifecycle callbacks.
         * The controllers can be retrieved by their keys from the map context.
         * @param cssClass The CSS class(es) to apply to the root of the rendered map component.
         * @returns A compiled map.
         */
        build(cssClass) {
            const context = this.buildContext();
            const controllers = [];
            const ref = exports.FSComponent.createRef();
            const onAfterRender = () => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onAfterMapRender(ref.instance);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onAfterMapRender() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onDeadZoneChanged = (deadZone) => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onDeadZoneChanged(deadZone);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onDeadZoneChanged() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onMapProjectionChanged = (mapProjection, changeFlags) => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onMapProjectionChanged(mapProjection, changeFlags);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onMapProjectionChanged() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onBeforeUpdated = (time, elapsed) => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onBeforeUpdated(time, elapsed);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onBeforeUpdated() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
                context.projection.applyQueued();
            };
            const onAfterUpdated = (time, elapsed) => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onAfterUpdated(time, elapsed);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onAfterUpdated() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onWake = () => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onWake();
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onWake() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onSleep = () => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onSleep();
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onSleep() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const onDestroy = () => {
                for (let i = 0; i < controllers.length; i++) {
                    if (!controllers[i].isAlive) {
                        controllers.splice(i, 1);
                        i--;
                    }
                    try {
                        controllers[i].onMapDestroyed();
                    }
                    catch (e) {
                        console.error(`MapSystem: error in controller .onMapDestroyed() callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
                for (const callback of this.destroyCallbacks.values()) {
                    try {
                        callback(context);
                    }
                    catch (e) {
                        console.error(`MapSystem: error in destroy callback: ${e}`);
                        if (e instanceof Error) {
                            console.error(e.stack);
                        }
                    }
                }
            };
            const map = (exports.FSComponent.buildComponent(MapSystemComponent, { ref: ref, model: context.model, projection: context.projection, bus: context.bus, projectedSize: this.projectedSize, onAfterRender: onAfterRender, onDeadZoneChanged: onDeadZoneChanged, onMapProjectionChanged: onMapProjectionChanged, onBeforeUpdated: onBeforeUpdated, onAfterUpdated: onAfterUpdated, onWake: onWake, onSleep: onSleep, onDestroy: onDestroy, class: cssClass }, Array.from(this.layerFactories.values()).sort((a, b) => a.order - b.order).map(factory => {
                const node = factory.factory(context);
                context.setLayer(factory.key, node.instance);
                return node;
            })));
            const controllerEntries = Array.from(this.controllerFactories)
                .map(([key, factory]) => [key, factory.factory(context)]);
            for (const [key, controller] of controllerEntries) {
                context.setController(key, controller);
            }
            controllers.push(...controllerEntries.map(([, controller]) => controller));
            for (const callback of this.initCallbacks.values()) {
                try {
                    callback(context);
                }
                catch (e) {
                    console.error(`MapSystem: error in init callback: ${e}`);
                    if (e instanceof Error) {
                        console.error(e.stack);
                    }
                }
            }
            return { context, map, ref };
        }
        /**
         * Builds a new map context. The map context will be initialized with all context properties and modules added to
         * this builder.
         * @returns The new map context.
         */
        buildContext() {
            var _a;
            const context = new DefaultMapSystemContext(this.bus, new MapProjection(this.projectedSize.get()[0], this.projectedSize.get()[1]), this.projectedSize, (_a = this.deadZone) !== null && _a !== void 0 ? _a : VecNSubject.createFromVector(new Float64Array(4)));
            context.projection.set({
                targetProjectedOffset: this.targetOffset,
                rangeEndpoints: this.nominalRangeEndpoints !== undefined
                    ? MapSystemUtils.nominalToTrueRelativeXY(this.nominalRangeEndpoints, context.projectedSize.get(), context.deadZone.get(), Vec2Math.create())
                    : undefined,
                range: this.range
            });
            for (const factory of Array.from(this.contextFactories.values()).sort((a, b) => a.order - b.order)) {
                context[factory.key] = factory.factory(context);
            }
            for (const factory of this.moduleFactories.values()) {
                context.model.addModule(factory.key, factory.factory(context));
            }
            return context;
        }
    }
    MapSystemBuilder.RESTRICTED_CONTEXT_KEYS = new Set([
        'bus',
        'model',
        'projection',
        'projectedSize',
        'deadZone',
        'getLayer',
        'setLayer',
        'getController',
        'setController'
    ]);

    /// <reference types="@microsoft/msfs-types/js/common" />
    /// <reference types="@microsoft/msfs-types/js/types" />
    /// <reference types="@microsoft/msfs-types/js/netbingmap" />
    /**
     * A synthetic vision display.
     */
    class SynVisComponent extends DisplayComponent {
        constructor() {
            super(...arguments);
            this.bingRef = exports.FSComponent.createRef();
            this.isRendered = false;
            this._isAwake = true;
            /**
             * A callback which is called when the Bing component is bound.
             */
            this.onBingBound = () => {
                // noop
            };
        }
        /** @inheritDoc */
        onAfterRender() {
            this.isRendered = true;
            if (!this._isAwake) {
                this.bingRef.instance.sleep();
            }
        }
        /**
         * Checks whether this display is awake.
         * @returns whether this display is awake.
         */
        isAwake() {
            return this._isAwake;
        }
        /**
         * Wakes this display. Upon awakening, this display will synchronize its state to the Bing instance to which it is
         * bound.
         */
        wake() {
            this._isAwake = true;
            if (this.isRendered) {
                this.bingRef.instance.wake();
            }
        }
        /**
         * Puts this display to sleep. While asleep, this display cannot make changes to the Bing instance to which it is
         * bound.
         */
        sleep() {
            this._isAwake = false;
            if (this.isRendered) {
                this.bingRef.instance.sleep();
            }
        }
        /** @inheritDoc */
        render() {
            return (exports.FSComponent.buildComponent(BingComponent, { ref: this.bingRef, id: this.props.bingId, mode: EBingMode.HORIZON, onBoundCallback: this.onBingBound, resolution: this.props.resolution, earthColors: this.props.earthColors, earthColorsElevationRange: this.props.earthColorsElevationRange, skyColor: this.props.skyColor, delay: this.props.bingDelay, skipUnbindOnDestroy: this.props.bingSkipUnbindOnDestroy, class: this.props.class }));
        }
        /** @inheritDoc */
        destroy() {
            var _a;
            (_a = this.bingRef.getOrDefault()) === null || _a === void 0 ? void 0 : _a.destroy();
            super.destroy();
        }
    }

    /** The acceptable priority types for a given warning. */
    exports.WarningType = void 0;
    (function (WarningType) {
        WarningType[WarningType["Warning"] = 0] = "Warning";
        WarningType[WarningType["Caution"] = 1] = "Caution";
        WarningType[WarningType["Test"] = 2] = "Test";
        WarningType[WarningType["SoundOnly"] = 3] = "SoundOnly";
    })(exports.WarningType || (exports.WarningType = {}));
    /** The main logic for a system warning. */
    class Warning {
        /**
         * Creates an instance of a Warning.
         * @param type The type of warning this is.
         * @param condition An XML logic element with the trigger logic.
         * @param shortText The warning message in short form.
         * @param longText The warning message in long form.
         * @param soundId The sound name to use with this warning.
         * @param once True if this warning only fires once
         */
        constructor(type, condition, shortText, longText, soundId, once) {
            this.type = type;
            this.shortText = shortText;
            this.longText = longText;
            this.soundId = soundId;
            this.condition = condition;
            this.once = once;
            this._triggered = false;
            if (this.soundId) {
                this._soundEventId = new Name_Z(this.soundId);
            }
        }
        /**
         * Whether or not we have any text at all.
         * @returns True if any non-empty text strings are set.
         */
        get hasText() {
            return this.shortText || this.longText ? true : false;
        }
        /**
         * The alert is being fired, take action.
         */
        trigger() {
            this._triggered = true;
        }
        /**
         * A text description for the warning, for debugging purposes.
         * @returns A string
         */
        get description() {
            return `<${this.shortText}|${this.longText}|${this.soundId}>`;
        }
        /**
         * Whether or not the warning has been triggered in this session.
         * @returns True if the warning has been triggered.
         */
        get triggered() {
            return this._triggered;
        }
        /**
         * Can this alert fire?
         * @returns True if the current configuration allows the alert to fire.
         */
        get canTrigger() {
            return !this.once || !this._triggered;
        }
        /**
         * The event ID Coherent returns when this sound has been played.
         * @returns A Name_Z based on the sound ID.
         */
        get eventId() {
            return this._soundEventId;
        }
    }
    /** The basic component for handling warning logic. */
    class WarningManager {
        /**
         * Create a WarningManager.
         * @param warnings An array of warnings to manage.
         * @param logicHost An event bus.
         * @param textCb A callback to display new warning text.
         * @param soundCb A callback to play an instrument sound from a sound ID.
         */
        constructor(warnings, logicHost, textCb, soundCb) {
            this.warnings = warnings;
            this.logicHost = logicHost;
            this.textCb = textCb;
            this.soundCb = soundCb;
            this.curSndIdx = null;
            this.curTxtIdx = null;
            this.warnActiveStates = new Array();
            for (let i = 0; i < warnings.length; i++) {
                this.logicHost.addLogicAsNumber(warnings[i].condition, this.handleWarning.bind(this, i), 0);
                this.warnActiveStates.push(false);
            }
        }
        /**
         * Handle a warning firing.  This is rather complex, but it basically keeps
         * track of every warning that is active, both for text and for sound, and
         * makes sure that the highest priority version of each is played or
         * displayed, masking and restoring lower priority warnings as needed.
         * @param warnIndex The index of our warnings array that's firing.
         * @param active 1 if the warning is active, 0 otherwise.
         */
        handleWarning(warnIndex, active) {
            const warning = this.warnings[warnIndex];
            // Handle a warning that is going active.
            if (active && warning.canTrigger) {
                this.warnActiveStates[warnIndex] = true;
                warning.trigger();
                // Only worry about text if the warning has text.
                if (warning.hasText) {
                    // If there is no existing text displayed, or text of a lower priority, replace it.
                    if ((this.curTxtIdx == undefined || this.curTxtIdx > warnIndex)) {
                        // First, if the prior warning was a one-shot, force it to inactive so it won't be restored later.
                        if (this.curTxtIdx && this.warnings[this.curTxtIdx].once) {
                            this.warnActiveStates[this.curTxtIdx] = false;
                        }
                        this.textCb(warning);
                        this.curTxtIdx = warnIndex;
                    }
                }
                // Now check for sound.
                if (this.soundCb !== undefined && warning.soundId) {
                    // Make sure we have the right to play our sound.
                    if (this.curSndIdx == undefined || this.curSndIdx > warnIndex) {
                        // Disable a prior sound, if one was playing.
                        if (this.curSndIdx && this.curSndIdx > warnIndex) {
                            this.soundCb(this.warnings[this.curSndIdx], false);
                            // If the prior warning was a one-shot, force it to inactive so it won't be restored.
                            if (this.warnings[this.curSndIdx].once) {
                                this.warnActiveStates[this.curSndIdx] = false;
                            }
                        }
                        this.soundCb(warning, true);
                        this.curSndIdx = warnIndex;
                    }
                }
                // If that was a one-time alert, turn it off, too.
                if (warning.once) {
                    this.handleWarning(warnIndex, 0);
                }
            }
            else if (!active) {
                this.warnActiveStates[warnIndex] = false;
                let isCurSnd = this.curSndIdx == warnIndex ? true : false;
                let isCurTxt = this.curTxtIdx == warnIndex ? true : false;
                // If this warning is the current active text or sound, we need to disable it.
                if (isCurSnd && this.soundCb) {
                    this.soundCb(warning, false);
                }
                if (isCurTxt) {
                    this.textCb(undefined);
                }
                // If either of those were true, let's see if there's anything else
                // of lower prirority that should be made active.
                if (isCurSnd || isCurTxt) {
                    // We know there was no higher-priority warning, so we step through
                    // everything lower than us.
                    let i = warnIndex + 1;
                    while ((isCurSnd || isCurTxt) && i < this.warnings.length) {
                        // Only continue this iteration if the next potential warning
                        // is active.
                        if (this.warnActiveStates[i]) {
                            const nextWarning = this.warnings[i];
                            // Make sure we can trigger the warning.   This will be false if
                            // it's a one-shot that's already triggered.  If so, just fall
                            // through to the next iteration.
                            if (nextWarning.canTrigger) {
                                // See if the lower priority warning has sound.  If so, activate.
                                if (isCurSnd && nextWarning.soundId && this.soundCb) {
                                    this.soundCb(this.warnings[i], true);
                                    this.curSndIdx = i;
                                    // We can stop looking for another sound.
                                    isCurSnd = false;
                                }
                                // Now do the same sort of thing for text.
                                if (isCurTxt && this.warnings[i].hasText) {
                                    this.textCb(this.warnings[i]);
                                    this.curTxtIdx = i;
                                    // We can stop looking for another text.
                                    isCurTxt = false;
                                }
                            }
                        }
                        // Move to the next warning on the list.
                        i++;
                    }
                }
                // If we haven't yet set new sound or text, just nullify them.
                if (isCurSnd && this.soundCb) {
                    this.curSndIdx = null;
                }
                if (isCurTxt) {
                    this.curTxtIdx = null;
                }
            }
        }
    }

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/utils/xmllogic" />
    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/baseinstrument" />
    /** Create a list of system warnings. */
    class XMLWarningFactory {
        /**
         * Create an XMLWarningFactory.
         * @param instrument The instrument that the warnings run in.
         */
        constructor(instrument) {
            this.instrument = instrument;
        }
        /**
         * Parse a panel.xml configuration to create a list of warnings.  The warning
         * priority is defined by their order in panel.xml, with higher priority
         * warnings coming sooner in the file.
         * @param document The configuration as an XML document.
         * @returns An array of Warnings
         */
        parseConfig(document) {
            const warnings = new Array();
            const configs = document.getElementsByTagName('VoicesAlerts');
            if (configs.length == 0) {
                return warnings;
            }
            const config = configs[0];
            for (const warn of config.children) {
                let type;
                const typeElem = warn.getElementsByTagName('Type');
                if (typeElem.length == 0) {
                    continue;
                }
                switch (typeElem[0].textContent) {
                    case 'Warning':
                        type = exports.WarningType.Warning;
                        break;
                    case 'Caution':
                        type = exports.WarningType.Caution;
                        break;
                    case 'Test':
                        type = exports.WarningType.Test;
                        break;
                    case 'SoundOnly':
                        type = exports.WarningType.SoundOnly;
                        break;
                    default:
                        continue;
                }
                let textElem = warn.getElementsByTagName('Condition');
                if (textElem.length == 0) {
                    continue;
                }
                const condition = new CompositeLogicXMLElement(this.instrument, textElem[0]);
                textElem = warn.getElementsByTagName('ShortText');
                let shortText = undefined;
                if (textElem.length > 0 && textElem[0].textContent !== null) {
                    shortText = textElem[0].textContent;
                }
                textElem = warn.getElementsByTagName('LongText');
                let longText = undefined;
                if (textElem.length > 0 && textElem[0].textContent !== null) {
                    longText = textElem[0].textContent;
                }
                textElem = warn.getElementsByTagName('SoundEvent');
                let soundEvent = undefined;
                if (textElem.length > 0 && textElem[0].textContent !== null) {
                    soundEvent = textElem[0].textContent;
                }
                textElem = warn.getElementsByTagName('Once');
                let once = false;
                if (textElem.length > 0 && textElem[0].textContent == 'True') {
                    once = true;
                }
                warnings.push(new Warning(type, condition, shortText, longText, soundEvent, once));
            }
            return warnings;
        }
    }

    /**
     * The style of cursor to use on a circular gauge.
     * This is treated as though it may have multiple options in the original
     * source.  For the sake of future expansion we'll make this an enum even
     * though it currently only has one option.  Maybe it can be used for future
     * expansion.
     */
    exports.XMLCircularGaugeCursor = void 0;
    (function (XMLCircularGaugeCursor) {
        /** Starting the enum at 1 to match its value in the stock XMLEngineDisplay.js */
        XMLCircularGaugeCursor[XMLCircularGaugeCursor["Triangle"] = 1] = "Triangle";
    })(exports.XMLCircularGaugeCursor || (exports.XMLCircularGaugeCursor = {}));
    /**
     * The possible locations for value text.
     * This is treated as though it may have multiple options in the original
     * source.  For the sake of future expansion we'll make this an enum even
     * though it currently only has one option.  Maybe it can be used for future
     * expansion.
     */
    exports.XMLCircularGaugeValuePos = void 0;
    (function (XMLCircularGaugeValuePos) {
        /** Starting the enum at 1 to match its value in the stock XMLEngineDisplay.js */
        XMLCircularGaugeValuePos[XMLCircularGaugeValuePos["End"] = 1] = "End";
    })(exports.XMLCircularGaugeValuePos || (exports.XMLCircularGaugeValuePos = {}));

    /**
     * This provides the valid values for the ValuePos tag on a horizontal gauge.
     */
    exports.XMLHorizontalGaugeValuePos = void 0;
    (function (XMLHorizontalGaugeValuePos) {
        /** Starting the enum at 1 to match its value in the stock XMLEngineDisplay.js */
        XMLHorizontalGaugeValuePos[XMLHorizontalGaugeValuePos["End"] = 1] = "End";
        XMLHorizontalGaugeValuePos[XMLHorizontalGaugeValuePos["Right"] = 2] = "Right";
    })(exports.XMLHorizontalGaugeValuePos || (exports.XMLHorizontalGaugeValuePos = {}));

    /**
     * This provides the valid values for the ValuePos tag on a vertical gauge.
     */
    exports.XMLVerticalGaugeValuePos = void 0;
    (function (XMLVerticalGaugeValuePos) {
        /** Starting the enum at 1 to match its value in the stock XMLEngineDisplay.js */
        XMLVerticalGaugeValuePos[XMLVerticalGaugeValuePos["None"] = 1] = "None";
    })(exports.XMLVerticalGaugeValuePos || (exports.XMLVerticalGaugeValuePos = {}));

    /**
     * The possible locations for value text.
     * This is treated as though it may have multiple options in the original
     * source.  For the sake of future expansion we'll make this an enum even
     * though it currently only has one option.  Maybe it can be used for future
     * expansion.
     */
    exports.XMLDoubleHorizontalGaugeValuePos = void 0;
    (function (XMLDoubleHorizontalGaugeValuePos) {
        /** Starting the enum at 2 to match its value in the stock XMLEngineDisplay.js */
        XMLDoubleHorizontalGaugeValuePos[XMLDoubleHorizontalGaugeValuePos["Right"] = 2] = "Right";
    })(exports.XMLDoubleHorizontalGaugeValuePos || (exports.XMLDoubleHorizontalGaugeValuePos = {}));

    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/utils/xmllogic" />
    /// <reference types="@microsoft/msfs-types/pages/vcockpit/instruments/shared/baseinstrument" />
    /**
     * The type of gauges available, as defined in XMLEngineDisplay.js.
     */
    exports.XMLGaugeType = void 0;
    (function (XMLGaugeType) {
        XMLGaugeType["Circular"] = "Circular";
        XMLGaugeType["Horizontal"] = "Horizontal";
        XMLGaugeType["DoubleHorizontal"] = "DoubleHorizontal";
        XMLGaugeType["Vertical"] = "Vertical";
        XMLGaugeType["DoubleVertical"] = "DoubleVertical";
        XMLGaugeType["Text"] = "Text";
        XMLGaugeType["ColumnGroup"] = "ColumnGroup";
        XMLGaugeType["Column"] = "Column";
        XMLGaugeType["Cylinder"] = "Cylinder";
        XMLGaugeType["TwinCylinder"] = "TwinCylinder";
    })(exports.XMLGaugeType || (exports.XMLGaugeType = {}));
    /**
     * Parse an XMLEngineDisplay configuration into an array of gauge specs.
     */
    class XMLGaugeConfigFactory {
        /**
         * Create an XMLGaugeConfigFactory.
         * @param instrument The instrument that holds this engine display.
         * @param bus An event bus for gauges that need it.
         */
        constructor(instrument, bus) {
            this.instrument = instrument;
            this.bus = bus;
        }
        /**
         * Convenience method to take a full XML instrument config and parse out the display config
         * section. This will check first to see if we are using an enhanced, multi-page config by
         * looking for an EnginePage tag in the EngineDisplay element.   If it finds it, it will
         * assume we have an advanced config, and return the content along with that of LeanPage
         * and SystemPag, if present.  If no EnginePage exists, we assume we're dealing with a
         * legacy configuration and just return the content of EngineDisplay itself as our engine
         * page with everything else undefined.
         * @param document The XML configuation document.
         * @returns An XMLEnhancedGaugeConfig with the full gauge configuration.
         */
        parseConfig(document) {
            var _a;
            const gaugeSpecs = new Array();
            const functions = new Map();
            const displayConfig = document.getElementsByTagName('EngineDisplay');
            if (displayConfig.length == 0) {
                return { override: false, functions: functions, enginePage: gaugeSpecs };
            }
            else {
                for (const func of document.getElementsByTagName('Function')) {
                    const funcSpec = this.makeFunction(func);
                    if (funcSpec !== undefined) {
                        functions.set(funcSpec.name, funcSpec);
                    }
                }
                const override = ((_a = displayConfig[0].getAttribute('override')) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'true';
                const enginePages = displayConfig[0].getElementsByTagName('EnginePage');
                if (enginePages.length == 0) {
                    return { override: override, functions: functions, enginePage: this._parseConfig(displayConfig[0]) };
                }
                const leanPages = displayConfig[0].getElementsByTagName('LeanPage');
                const systemPages = displayConfig[0].getElementsByTagName('SystemPage');
                return {
                    override: override,
                    functions: functions,
                    enginePage: this._parseConfig(enginePages[0]),
                    leanPage: leanPages.length > 0 ? this._parseConfig(leanPages[0]) : undefined,
                    systemPage: systemPages.length > 0 ? this._parseConfig(systemPages[0]) : undefined
                };
            }
        }
        /**
         * Parse an engine display setup.
         * @param config An instrument XML config document.
         * @returns An array of the gauges defined in the configuration.
         */
        _parseConfig(config) {
            var _a;
            const gaugeSpecs = new Array();
            if (config.children.length == 0) {
                return gaugeSpecs;
            }
            for (const gauge of config.children) {
                switch (gauge.tagName) {
                    case 'Gauge':
                        switch ((_a = gauge.getElementsByTagName('Type')[0]) === null || _a === void 0 ? void 0 : _a.textContent) {
                            case 'Circular':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.Circular,
                                    configuration: this.createCircularGauge(gauge)
                                });
                                break;
                            case 'Horizontal':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.Horizontal,
                                    configuration: this.createHorizontalGauge(gauge)
                                });
                                break;
                            case 'DoubleHorizontal':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.DoubleHorizontal,
                                    configuration: this.createDoubleHorizontalGauge(gauge)
                                });
                                break;
                            case 'Vertical':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.Vertical,
                                    configuration: this.createVerticalGauge(gauge)
                                });
                                break;
                            case 'DoubleVertical':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.DoubleVertical,
                                    configuration: this.createDoubleVerticalGauge(gauge)
                                });
                                break;
                            case 'Cylinder':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.Cylinder,
                                    configuration: this.createCylinderGauge(gauge)
                                });
                                break;
                            case 'TwinCylinder':
                                gaugeSpecs.push({
                                    gaugeType: exports.XMLGaugeType.TwinCylinder,
                                    configuration: this.createCylinderGauge(gauge)
                                });
                                break;
                        }
                        break;
                    case 'Text':
                        {
                            const textProps = {};
                            const className = gauge.getAttribute('id');
                            if (className !== null) {
                                textProps.class = className;
                            }
                            const leftElem = gauge.getElementsByTagName('Left');
                            if (leftElem.length > 0) {
                                textProps.left = this.makeTextColumn(leftElem[0]);
                            }
                            const centerElem = gauge.getElementsByTagName('Center');
                            if (centerElem.length > 0) {
                                textProps.center = this.makeTextColumn(centerElem[0]);
                            }
                            const rightElem = gauge.getElementsByTagName('Right');
                            if (rightElem.length > 0) {
                                textProps.right = this.makeTextColumn(rightElem[0]);
                            }
                            const style = XMLGaugeConfigFactory.parseStyleDefinition(gauge.getElementsByTagName('Style'));
                            if (style !== undefined) {
                                textProps.style = style;
                            }
                            gaugeSpecs.push({
                                gaugeType: exports.XMLGaugeType.Text,
                                configuration: textProps
                            });
                        }
                        break;
                    case 'ColumnGroup':
                        gaugeSpecs.push({
                            gaugeType: exports.XMLGaugeType.ColumnGroup,
                            configuration: this.createColumnGroup(gauge)
                        });
                        break;
                    case 'Column':
                        gaugeSpecs.push({
                            gaugeType: exports.XMLGaugeType.Column,
                            configuration: this.createColumn(gauge)
                        });
                        break;
                }
            }
            return gaugeSpecs;
        }
        /**
         * Construct a single column of text for a text element.  This can be any
         * one of Left, Right, or Center.
         * @param columnDef The XML definition for the given column.
         * @returns an XMLTextColumn configuration.
         */
        makeTextColumn(columnDef) {
            const contentElem = columnDef.getElementsByTagName('Content');
            const config = {
                content: new CompositeLogicXMLElement(this.instrument, contentElem.length > 0 ? contentElem[0] : columnDef)
            };
            const colorElem = columnDef.getElementsByTagName('Color');
            if (colorElem.length > 0) {
                config.color = new CompositeLogicXMLElement(this.instrument, colorElem[0]);
            }
            const className = columnDef.getAttribute('id');
            if (className !== null) {
                config.class = className;
            }
            const fontSize = columnDef.getAttribute('fontsize');
            if (fontSize !== null) {
                config.fontSize = fontSize;
            }
            return config;
        }
        /**
         * Make a function.
         * @param functionDef The XML definition for the function.
         * @returns an XMLFunction type or undefined if there's an error
         */
        makeFunction(functionDef) {
            const name = functionDef.getAttribute('Name');
            if (!name || functionDef.children.length == 0) {
                return undefined;
            }
            return {
                name: name,
                logic: new CompositeLogicXMLElement(this.instrument, functionDef)
            };
        }
        /**
         * Create a base XMLGaugeProps definition.  This will be combined with the
         * props for a speciific gauge type to fully define the config interface.
         * @param gauge The gauge definition
         * @returns A set of XMLGaugeProps
         */
        parseGaugeDefinition(gauge) {
            var _a;
            // TODO Maybe make this use getAndAssign, too?
            const props = {};
            /**
             * A closure to make our variable assignments easier.
             * @param prop The property we want to assign.
             * @param tag The HTML tag to get the value from.
             * @param converter A converter function.
             */
            const assign = (prop, tag, converter = (v) => { return v; }) => {
                XMLGaugeConfigFactory.getAndAssign(props, gauge, prop, tag, converter);
            };
            const colorZones = this.makeColorZones(gauge.getElementsByTagName('ColorZone'));
            if (colorZones !== undefined) {
                props.colorZones = colorZones;
            }
            const colorLines = this.makeColorLines(gauge.getElementsByTagName('ColorLine'));
            if (colorLines !== undefined) {
                props.colorLines = colorLines;
            }
            const referenceBugs = this.makeReferenceBugs(gauge.getElementsByTagName('ReferenceBug'));
            if (referenceBugs !== undefined) {
                props.referenceBugs = referenceBugs;
            }
            const createLogicElement = (el) => {
                if (el !== undefined) {
                    return new CompositeLogicXMLElement(this.instrument, el);
                }
                return undefined;
            };
            props.minimum = createLogicElement(gauge.getElementsByTagName('Minimum')[0]);
            props.maximum = createLogicElement(gauge.getElementsByTagName('Maximum')[0]);
            props.value1 = createLogicElement(gauge.getElementsByTagName('Value')[0]);
            props.value2 = createLogicElement(gauge.getElementsByTagName('Value2')[0]);
            assign('title', 'Title', (v) => { return v ? v : ''; });
            assign('unit', 'Unit', (v) => { return v ? v : ''; });
            assign('graduationLength', 'GraduationLength', parseFloat);
            props.graduationHasText = ((_a = gauge.getElementsByTagName('GraduationLength')[0]) === null || _a === void 0 ? void 0 : _a.getAttribute('text')) == 'True';
            assign('beginText', 'BeginText');
            assign('endText', 'EndText');
            assign('cursorText1', 'CursorText', (v) => { return v ? v : ''; });
            assign('cursorText2', 'CursorText2', (v) => { return v ? v : ''; });
            assign('id', 'ID');
            props.redBlink = createLogicElement(gauge.getElementsByTagName('RedBlink')[0]);
            return props;
        }
        // The logic for creating these gauges is a little intricate and repeats a number of times.
        // To avoid having redundant comments, here's the general plan for what's happening.
        //
        // First, we create an instance of the gauge's style interface in several steps. These
        // take advantage of the fact that almost all of the props on on the interfaces are optional
        // to allow us to compose the gauge-specific interface in pieces.
        //
        // 1) The gauge-specific create function passes the Style element to the generic
        //    parseStyleDefinition function, which returns an interface that has all of the
        //    universal style properties.
        // 2) The function then creates its own gauge-specific style interface using parsing logic
        //    unique to the gauge.
        // 3) The values of the generic interface are then assigned to the object-specific one so
        //    that we have one interface with all the styling information needed.
        //
        // Next, we repeat the same process with the rest of the gauge definitions.   At this point,
        // the primary way in which the shapes of the interfaces differ is in what their style
        // definitions look like, so the second phase is just another assignment compositing the
        // custom-derived style and the remainder of the generic definiton as retrieved from the
        // parseGaugeDefinition method.
        //
        // We play a bit fast and loose with properties here and don't really do any confirmation
        // that the gauge definitions we get are valid.  The user could, for example, provide a
        // <Value2> property to a gauge that only has one value and that would be populated in the
        // configuration.   It would be harmless, because it would be ignored by the gauge code,
        // but it's still kind of gross.
        //
        // This models how the standard XMLEngineDisplay.js works.  In the future we might want to
        // tighten this up with better type checking and error throwing, in which case we can expand
        // these functions to use a bit more logic in this second phase when they're composing
        // the final configuration instance.
        //
        // Ok, on with the show.
        /**
         * Create a circular gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createCircularGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            let style = {};
            const innerElem = styleElem[0];
            if (innerElem !== undefined) {
                /**
                 * A closure to make our variable assignments easier.
                 * @param prop The property we want to assign.
                 * @param tag The HTML tag to get the value from.
                 * @param converter A converter function.
                 */
                const assign = (prop, tag, converter = (v) => { return v; }) => {
                    XMLGaugeConfigFactory.getAndAssign(style, innerElem, prop, tag, converter);
                };
                assign('forceTextColor', 'ForceTextColor');
                assign('textIncrement', 'TextIncrement', parseFloat);
                assign('beginAngle', 'BeginAngle', parseFloat);
                assign('endAngle', 'EndAngle', parseFloat);
                assign('cursorType', 'CursorType', (v) => { return v == 'Triangle' ? exports.XMLCircularGaugeCursor.Triangle : undefined; });
                assign('valuePos', 'ValuePos', (v) => { return v == 'End' ? exports.XMLCircularGaugeValuePos.End : undefined; });
                assign('valuePrecision', 'ValuePrecision', parseInt);
            }
            style = Object.assign(style, genericStyle);
            return Object.assign({ style: style }, this.parseGaugeDefinition(gaugeDef));
        }
        /**
         * Create a horizontal gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createHorizontalGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            let style = {};
            const innerElem = styleElem[0];
            if (innerElem !== undefined) {
                /**
                 * A closure to make our variable assignments easier.
                 * @param prop The property we want to assign.
                 * @param tag The HTML tag to get the value from.
                 * @param converter A converter function.
                 */
                const assign = (prop, tag, converter = (v) => { return v; }) => {
                    XMLGaugeConfigFactory.getAndAssign(style, innerElem, prop, tag, converter);
                };
                assign('valuePos', 'ValuePos', (v) => {
                    switch (v) {
                        case 'Right':
                            return exports.XMLHorizontalGaugeValuePos.Right;
                        case 'End':
                            return exports.XMLHorizontalGaugeValuePos.End;
                        default:
                            return undefined;
                    }
                });
                assign('textIncrement', 'TextIncrement', parseFloat);
                assign('cursorColor', 'CursorColor');
                assign('width', 'Width', parseFloat);
                assign('reverseY', 'ReverseY', (v) => { return v == 'True'; });
                assign('valuePrecision', 'ValuePrecision', parseInt);
            }
            style = Object.assign(style, genericStyle);
            return Object.assign({ style: style }, this.parseGaugeDefinition(gaugeDef));
        }
        /**
         * Create a double horizontal gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createDoubleHorizontalGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            let style = {};
            if (styleElem[0] !== undefined) {
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'textIncrement', 'TextIncrement', parseFloat);
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'valuePrecision', 'ValuePrecision', parseInt);
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'valuePos', 'ValuePos', (v) => {
                    return v == 'Right' ? exports.XMLDoubleHorizontalGaugeValuePos.Right : undefined;
                });
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'pointerStyle', 'PointerStyle', (v) => {
                    return v == 'Arrow' ? 'arrow' : 'standard';
                });
            }
            style = Object.assign(style, genericStyle);
            return Object.assign({ style: style }, this.parseGaugeDefinition(gaugeDef));
        }
        /**
         * Create a single vertical gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createVerticalGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            let style = {};
            if (styleElem[0] !== undefined) {
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'textIncrement', 'TextIncrement', parseFloat);
            }
            style = Object.assign(style, genericStyle);
            return Object.assign({ style: style }, this.parseGaugeDefinition(gaugeDef));
        }
        /**
         * Create a double vertical gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createDoubleVerticalGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            let style = {};
            if (styleElem[0] !== undefined) {
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'textIncrement', 'TextIncrement', parseFloat);
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'height', 'Height', parseFloat);
            }
            style = Object.assign(style, genericStyle);
            return Object.assign({ style: style }, this.parseGaugeDefinition(gaugeDef));
        }
        /**
         * Create a cylinder gauge.
         * @param gaugeDef An XML element defining the gauge.
         * @returns The props for this gauge.
         */
        createCylinderGauge(gaugeDef) {
            const styleElem = gaugeDef.getElementsByTagName('Style');
            const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
            const columnElems = gaugeDef.getElementsByTagName('Columns');
            const rowElems = gaugeDef.getElementsByTagName('Rows');
            const config = this.parseGaugeDefinition(gaugeDef);
            config.bus = this.bus;
            let style = {};
            if (styleElem[0] !== undefined) {
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'textIncrement', 'TextIncrement', parseFloat);
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'redline', 'ShowRedline', (text) => { return text == 'True'; });
                XMLGaugeConfigFactory.getAndAssign(style, styleElem[0], 'peakTemps', 'ShowPeak', (text) => { return text == 'True'; });
            }
            style = Object.assign(style, genericStyle);
            config.style = style;
            if (columnElems.length > 0) {
                config.numColumns = new CompositeLogicXMLElement(this.instrument, columnElems[0]);
            }
            if (rowElems.length > 0) {
                config.numRows = new CompositeLogicXMLElement(this.instrument, rowElems[0]);
            }
            XMLGaugeConfigFactory.getAndAssign(config, gaugeDef, 'tempOrder', 'TempOrder', (text) => {
                const tempOrder = new Array();
                for (const item of text.split(',')) {
                    tempOrder.push(parseInt(item));
                }
                return tempOrder;
            });
            return config;
        }
        /**
         * Create a column group.
         * @param gaugeDef AN XML element defining the group.
         * @returns The props for the group with all contained columns.
         */
        createColumnGroup(gaugeDef) {
            const columns = new Array();
            const children = gaugeDef.children;
            for (const child of children) {
                if (child.tagName == 'Column') {
                    columns.push(this.createColumn(child));
                }
            }
            const group = {
                bus: this.bus,
                columns: columns
            };
            XMLGaugeConfigFactory.getAndAssign(group, gaugeDef, 'id', 'id');
            return group;
        }
        /**
         * Create a column of gauges.
         * @param gaugeDef An XML element defining the column.
         * @returns The props of the column with all contained gauges.
         */
        createColumn(gaugeDef) {
            const column = { gauges: this._parseConfig(gaugeDef) };
            XMLGaugeConfigFactory.getAndAssign(column, gaugeDef, 'id', 'id');
            XMLGaugeConfigFactory.getAndAssign(column, gaugeDef, 'width', 'width', parseFloat);
            return column;
        }
        // Utility functions.
        /**
         * Check the value of a setting and, if it's defined, assign it to the
         * property of an object with optional type conversion.
         * @param obj The object to manipulate.
         * @param elem The element to get the value from.
         * @param prop The name of the property to set.
         * @param tag The tag name to retrieve.
         * @param converter A type conversion used if the value is defined.
         */
        static getAndAssign(obj, elem, prop, tag, converter = (val) => { return val; }) {
            var _a;
            const value = (_a = elem.getElementsByTagName(tag)[0]) === null || _a === void 0 ? void 0 : _a.textContent;
            if (value === null || value === undefined) {
                return;
            }
            const newVal = converter(value);
            if (newVal !== undefined) {
                obj[prop] = newVal;
            }
        }
        /**
         * Create a basic XML style from a gauge definition.
         * @param styleDoc A style definition block
         * @returns An XMLGaugeStyle
         */
        static parseStyleDefinition(styleDoc) {
            var _a;
            const style = {};
            if (styleDoc.length > 0) {
                XMLGaugeConfigFactory.getAndAssign(style, styleDoc[0], 'sizePercent', 'SizePercent', parseFloat);
                const marginsElem = styleDoc[0].getElementsByTagName('Margins');
                if (marginsElem.length > 0 && ((_a = marginsElem[0]) === null || _a === void 0 ? void 0 : _a.textContent) !== null) {
                    XMLGaugeConfigFactory.getAndAssign(style, marginsElem[0], 'marginLeft', 'Left', parseFloat);
                    XMLGaugeConfigFactory.getAndAssign(style, marginsElem[0], 'marginRight', 'Right', parseFloat);
                    XMLGaugeConfigFactory.getAndAssign(style, marginsElem[0], 'marginTop', 'Top', parseFloat);
                    XMLGaugeConfigFactory.getAndAssign(style, marginsElem[0], 'marginBottom', 'Bottom', parseFloat);
                }
            }
            return style;
        }
        /**
         * Get the SmoothFactor value from a gauge definition if present.
         * @param element The HTML element to search for the parameter.
         * @returns The smoothing factor as a number, or undefined if not found.
         */
        parseSmoothFactor(element) {
            var _a;
            const smoothElem = element.getElementsByTagName('SmoothFactor');
            if (smoothElem.length > 0 && ((_a = smoothElem[0]) === null || _a === void 0 ? void 0 : _a.textContent) !== null) {
                return smoothElem.length > 0 ? parseFloat(smoothElem[0].textContent) : undefined;
            }
            return undefined;
        }
        /**
         * Create an array of color zones if a definition exists.
         * @param zones An array of color zone definitions.
         * @returns An array of XMLGaugeColorZones
         */
        makeColorZones(zones) {
            var _a, _b;
            const zoneArray = new Array();
            for (let i = 0; i < zones.length; i++) {
                let color = 'white';
                const colorElem = zones[i].getElementsByTagName('Color');
                if (colorElem.length > 0) {
                    color = ((_a = colorElem[0]) === null || _a === void 0 ? void 0 : _a.textContent) ? (_b = colorElem[0]) === null || _b === void 0 ? void 0 : _b.textContent : 'white';
                }
                zoneArray.push({
                    color: color,
                    begin: new CompositeLogicXMLElement(this.instrument, zones[i].getElementsByTagName('Begin')[0]),
                    end: new CompositeLogicXMLElement(this.instrument, zones[i].getElementsByTagName('End')[0]),
                    smoothFactor: this.parseSmoothFactor(zones[i])
                });
            }
            return zoneArray.length > 0 ? zoneArray : undefined;
        }
        /**
         * Create an array of color lines if a definition exists.
         * @param lines An array of color line definitions.
         * @returns An array of XMLGaugeColorLines
         */
        makeColorLines(lines) {
            var _a, _b;
            const lineArray = new Array();
            for (let i = 0; i < lines.length; i++) {
                let color = 'white';
                const colorElem = lines[i].getElementsByTagName('Color');
                if (colorElem.length > 0) {
                    color = ((_a = colorElem[0]) === null || _a === void 0 ? void 0 : _a.textContent) ? (_b = colorElem[0]) === null || _b === void 0 ? void 0 : _b.textContent : 'white';
                }
                lineArray.push({
                    color: color,
                    position: new CompositeLogicXMLElement(this.instrument, lines[i].getElementsByTagName('Position')[0]),
                    smoothFactor: this.parseSmoothFactor(lines[i])
                });
            }
            return lineArray.length > 0 ? lineArray : undefined;
        }
        /**
         * Create an array of reference bugs if a definition exists.
         * @param bugs An array of reference bug definitions.
         * @returns An array of XMLGaugeReferenceBugs
         */
        makeReferenceBugs(bugs) {
            const bugArray = new Array();
            for (let i = 0; i < bugs.length; i++) {
                const styleElem = bugs[i].getElementsByTagName('Style');
                const genericStyle = XMLGaugeConfigFactory.parseStyleDefinition(styleElem);
                let bugStyle = {};
                const innerElem = styleElem[0];
                if (innerElem !== undefined) {
                    XMLGaugeConfigFactory.getAndAssign(bugStyle, innerElem, 'color', 'Color');
                }
                bugStyle = Object.assign(bugStyle, genericStyle);
                bugArray.push({
                    position: new CompositeLogicXMLElement(this.instrument, bugs[i].getElementsByTagName('Position')[0]),
                    displayLogic: new CompositeLogicXMLElement(this.instrument, bugs[i].getElementsByTagName('DisplayLogic')[0]),
                    style: bugStyle,
                    smoothFactor: this.parseSmoothFactor(bugs[i])
                });
            }
            return bugArray;
        }
    }

    /** An alert can be either new or acknowledged. */
    var AlertState;
    (function (AlertState) {
        /** A newly arrived, unackowledged alert message. */
        AlertState[AlertState["New"] = 0] = "New";
        /** An alert message that has been acknowledged. */
        AlertState[AlertState["Acked"] = 1] = "Acked";
    })(AlertState || (AlertState = {}));
    /** A CAS alert manager. */
    class SystemAlertManager {
        /**
         * Create a SystemAlertManager instance.
         * @param bus The event bus
         * @param annunciations An array of the system annunciations to monitor
         * @param logicHost An actively updated composite logic host.
         * @param warningSoundId The identifier of the warning sound, if other than default
         * @param cautionSoundId The identifier of the caution sound, if other than default
         */
        constructor(bus, annunciations, logicHost, warningSoundId = 'tone_warning', cautionSoundId = 'tone_caution') {
            this.initialized = false;
            this.activeAnns = [];
            this.bus = bus;
            this.logicHost = logicHost;
            this.soundPublisher = this.bus.getPublisher();
            this.alertPublisher = this.bus.getPublisher();
            this.warningSoundId = warningSoundId;
            this.cautionSoundId = cautionSoundId;
            this.annunciations = annunciations;
            KeyEventManager.getManager(this.bus).then(manager => {
                manager.interceptKey('MASTER_CAUTION_ACKNOWLEDGE', true);
                manager.interceptKey('MASTER_WARNING_ACKNOWLEDGE', true);
            });
            this.bus.getSubscriber().on('key_intercept').handle((keyData) => {
                switch (keyData.key) {
                    case 'MASTER_CAUTION_ACKNOWLEDGE':
                        this.handleAcknowledgement(exports.AnnunciationType.Caution);
                        break;
                    case 'MASTER_WARNING_ACKNOWLEDGE':
                        this.handleAcknowledgement(exports.AnnunciationType.Warning);
                        break;
                }
            });
            for (let i = 0; i < this.annunciations.length; i++) {
                const ann = this.annunciations[i];
                this.logicHost.addLogicAsNumber(ann.condition, (v) => {
                    if (v == 1) {
                        this.handleAnnunciationActive(i);
                    }
                    else {
                        this.handleAnnunciationInactive(i);
                    }
                }, 0);
            }
            const gameStateSub = GameStateProvider.get().sub(state => {
                if (state === GameState.ingame) {
                    // Reading game states does not seem to be sufficient to keep from getting alarms
                    // based on things that are active when the system is initialized, so we'll just
                    // set a brief timeout here to take care of that.
                    setTimeout(() => {
                        this.initialized = true;
                        this.alertPublisher.pub('master_acknowledge', exports.AnnunciationType.Caution, true, false);
                        this.alertPublisher.pub('master_acknowledge', exports.AnnunciationType.Warning, true, false);
                    }, 5000);
                    gameStateSub.destroy();
                }
            }, false, true);
            gameStateSub.resume(true);
            this.setMasterStatus(exports.AnnunciationType.Caution, false);
            this.setMasterStatus(exports.AnnunciationType.Warning, false);
        }
        /**
         * Set both sets of simvars relevant to a master caution or warning status.
         * @param type The type of the status to set
         * @param active Whether or not the status is active
         */
        setMasterStatus(type, active) {
            switch (type) {
                case exports.AnnunciationType.Caution:
                    SimVar.SetSimVarValue('K:MASTER_CAUTION_SET', 'bool', active);
                    SimVar.SetSimVarValue('L:Generic_Master_Caution_Active', 'bool', active);
                    break;
                case exports.AnnunciationType.Warning:
                    SimVar.SetSimVarValue('K:MASTER_WARNING_SET', 'bool', active);
                    SimVar.SetSimVarValue('L:Generic_Master_Warning_Active', 'bool', active);
                    break;
            }
        }
        /**
         * Handle an annunciation going active.
         * @param idx The index of the annunciations array for the annunciation.
         */
        handleAnnunciationActive(idx) {
            const type = this.annunciations[idx].type;
            if ((!this.checkForActiveType(type)) && this.initialized) {
                this.setMasterStatus(type, true);
                if (type == exports.AnnunciationType.Caution) {
                    this.soundPublisher.pub('sound_server_play_sound', this.cautionSoundId, true, false);
                }
                else if (type == exports.AnnunciationType.Warning) {
                    this.soundPublisher.pub('sound_server_start_sound', this.warningSoundId, true, false);
                }
            }
            this.addOrUpdateAnnunciation(idx);
        }
        /**
         * Handle an annunciation going inactive.
         * @param idx The index of the annunciations array for the annunciation.
         */
        handleAnnunciationInactive(idx) {
            const type = this.annunciations[idx].type;
            this.removeAnnunciation(idx);
            if (!this.checkForActiveType(type) && this.initialized) {
                this.setMasterStatus(this.annunciations[idx].type, false);
                if (type == exports.AnnunciationType.Warning) {
                    this.soundPublisher.pub('sound_server_stop_sound', this.warningSoundId, true, false);
                }
            }
        }
        /**
         * Handle a master warning or caution acknowledgement.
         * @param type The type of alert to acknowledge.
         */
        handleAcknowledgement(type) {
            let updated = false;
            this.setMasterStatus(type, false);
            this.alertPublisher.pub('master_acknowledge', type, true, false);
            for (let i = 0; i < this.activeAnns.length; i++) {
                if (this.annunciations[this.activeAnns[i].index].type === type) {
                    this.activeAnns[i].state = AlertState.Acked;
                    updated = true;
                }
            }
            if (updated) {
                // If the acknowledgement was for warnings, stop playing the warning tone.
                if (type == exports.AnnunciationType.Warning) {
                    this.soundPublisher.pub('sound_server_stop_sound', this.warningSoundId, true, false);
                }
            }
        }
        /**
         * See if there is still an active, unacked annunciation of the given type.
         * @param type The annunciation type to check for.
         * @returns True if there is an active, unacked annunciation of the given type, false otherwise.
         */
        checkForActiveType(type) {
            for (let i = 0; i < this.activeAnns.length; i++) {
                if (this.annunciations[this.activeAnns[i].index].type === type && this.activeAnns[i].state === AlertState.New) {
                    return true;
                }
            }
            return false;
        }
        /**
         * Add an annunciation to the active list if it's new, or update it if already there.
         * @param idx The index of the annunciations array for the annunciation.
         * @returns The index of the added or updated annunciation in the active list.
         */
        addOrUpdateAnnunciation(idx) {
            // Any message that comes in before we're initialized is considered acknowledged.
            const adjustedState = this.initialized ? AlertState.New : AlertState.Acked;
            this.alertPublisher.pub('alert_triggered', idx, true, false);
            for (let i = 0; i < this.activeAnns.length; i++) {
                if (this.activeAnns[i].index === idx) {
                    this.activeAnns[i].state = adjustedState;
                    return i;
                }
            }
            this.activeAnns.push({
                index: idx,
                state: adjustedState,
            });
            return this.activeAnns.length - 1;
        }
        /**
         * Remove an annunciation from the active list if present.
         * @param idx The index of the annunciations array for the annunciation.
         * @returns True if the annunciation was removed, false otherwise.
         */
        removeAnnunciation(idx) {
            for (let i = 0; i < this.activeAnns.length; i++) {
                if (this.activeAnns[i].index === idx) {
                    this.activeAnns.splice(i, 1);
                    this.alertPublisher.pub('alert_cleared', idx, true, false);
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * A utility class for working with VNAV.
     */
    class VNavUtils {
        /**
         * Checks if a constraint is a user-created constraint.
         * @param lateralLeg The Lateral Flight Plan Leg.
         * @returns If this constraint is a user-created constraint.
         */
        static isUserConstraint(lateralLeg) {
            if (lateralLeg.verticalData.altDesc !== lateralLeg.leg.altDesc
                || lateralLeg.verticalData.altitude1 !== lateralLeg.leg.altitude1
                || lateralLeg.verticalData.altitude2 !== lateralLeg.leg.altitude2) {
                return true;
            }
            return false;
        }
        /**
         * Gets the required vertical speed to meet an altitude constraint.
         * @param distance The distance to the constraint, in nautical miles.
         * @param targetAltitude The target altitude for the constraint, in feet.
         * @param currentAltitude The current altitude, in feet.
         * @param groundSpeed The current groundspeed, in knots.
         * @returns The required vertical speed, in feet per minute, to meet the altitude constraint.
         */
        static getRequiredVs(distance, targetAltitude, currentAltitude, groundSpeed) {
            const delta = targetAltitude - currentAltitude;
            const minutesToConstraint = distance / groundSpeed * 60;
            return delta / minutesToConstraint;
        }
        /**
         * Gets the vertical speed required to maintain a given flight path angle and groundspeed.
         * @param fpa The flight path angle, in degrees. Positive angles represent an ascending flight path.
         * @param groundspeed The groundspeed, in knots.
         * @returns The vertical speed required to maintain the specified flight path angle and groundspeed.
         */
        static getVerticalSpeedFromFpa(fpa, groundspeed) {
            return UnitType.NMILE.convertTo(groundspeed / 60, UnitType.FOOT) * Math.tan(fpa * Avionics.Utils.DEG2RAD);
        }
        /**
         * Gets the equivalent flight path angle for a given vertical speed and groundspeed. For this calculation, positive
         * flight path angles represent an ascending flight path.
         * @param vs The vertical speed, in feet per minute.
         * @param groundspeed The groundspeed, in knots.
         * @returns The flight path angle equivalent to the specified vertical speed and ground speed.
         */
        static getFpaFromVerticalSpeed(vs, groundspeed) {
            return this.getFpa(UnitType.NMILE.convertTo(groundspeed / 60, UnitType.FOOT), vs);
        }
        /**
         * Gets the flight path angle required to travel a given lateral distance and altitude. Positive flight path angles
         * represent an ascending flight path. By convention, the flight path angle required to travel zero altitude equals
         * zero degrees for all distances.
         * @param distance The lateral distance to travel, in the same units as `altitude`.
         * @param altitude The altitude to travel, in the same units as `distance`.
         * @returns The flight path angle, in degrees, required to travel the specified altitude and distance.
         */
        static getFpa(distance, altitude) {
            if (altitude === 0) {
                return 0;
            }
            return UnitType.RADIAN.convertTo(Math.atan(altitude / distance), UnitType.DEGREE);
        }
        /**
         * Gets the change in altitude along a flight path angle for a given lateral distance traveled.
         * @param fpa The flight path angle, in degrees. Positive values represent an ascending flight path.
         * @param distance The lateral distance traveled.
         * @returns The change in altitude along the specified flight path angle for the specified lateral distance traveled,
         * expressed in the same units as `distance`.
         */
        static altitudeForDistance(fpa, distance) {
            return Math.tan(UnitType.DEGREE.convertTo(fpa, UnitType.RADIAN)) * distance;
        }
        /**
         * Gets the lateral distance traveled along a flight path angle for a given change in altitude.
         * @param fpa The flight path angle, in degrees. Positive values represent an ascending flight path.
         * @param altitude The change in the altitude.
         * @returns The lateral distance traveled along the specified flight path angle for the specified change in altitude,
         * expressed in the same units as `altitude`.
         */
        static distanceForAltitude(fpa, altitude) {
            return altitude / Math.tan(UnitType.DEGREE.convertTo(fpa, UnitType.RADIAN));
        }
        /**
         * Gets the missed approach leg index.
         * @param plan The flight plan.
         * @returns The Destination leg global leg index.
         */
        static getMissedApproachLegIndex(plan) {
            if (plan.length > 0) {
                for (let l = plan.length - 1; l > 0; l--) {
                    const planLeg = plan.tryGetLeg(l);
                    if (planLeg && BitFlags.isAll(planLeg.leg.fixTypeFlags, exports.FixTypeFlags.MAP)) {
                        return l;
                    }
                }
            }
            return Math.max(0, plan.length - 1);
        }
        /**
         * Gets the FAF index in the plan.
         * @param plan The flight plan.
         * @returns The FAF index in the plan.
         */
        static getFafIndex(plan) {
            if (plan.length > 0) {
                for (let l = plan.length - 1; l > 0; l--) {
                    const planLeg = plan.tryGetLeg(l);
                    if (planLeg && BitFlags.isAll(planLeg.leg.fixTypeFlags, exports.FixTypeFlags.FAF)) {
                        return l;
                    }
                }
            }
            return undefined;
        }
        /**
         * Finds and returns the FAF index in the plan.
         * @param lateralPlan The lateral flight plan.
         * @param iterator The FlightPlanLegIterator instance.
         * @returns The FAF index in the lateral flight plan.
         */
        static getFafIndexReverse(lateralPlan, iterator) {
            let fafIndex = -1;
            iterator.iterateReverse(lateralPlan, cursor => {
                if (fafIndex === -1 && cursor.legDefinition && (cursor.legDefinition.leg.fixTypeFlags & exports.FixTypeFlags.FAF)) {
                    fafIndex = cursor.legIndex + cursor.segment.offset;
                }
            });
            fafIndex = fafIndex > -1 ? fafIndex : fafIndex = Math.max(0, lateralPlan.length - 1);
            return fafIndex;
        }
        /**
         * Gets the index of the VNAV constraint that contains a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index of the flight plan leg.
         * @returns The index of the VNAV constraint that contains the specified flight plan leg, or `-1` if one could not
         * be found.
         */
        static getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex) {
            for (let c = verticalPlan.constraints.length - 1; c >= 0; c--) {
                if (verticalPlan.constraints[c].index >= globalLegIndex) {
                    return c;
                }
            }
            return -1;
        }
        /**
         * Gets the VNAV constraint that contains a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index of the flight plan leg.
         * @returns The VNAV constraint that contains the specified flight plan leg, or `undefined` if one could not be
         * found.
         */
        static getConstraintFromLegIndex(verticalPlan, globalLegIndex) {
            return verticalPlan.constraints[VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex)];
        }
        /**
         * Gets the index of the VNAV constraint immediately prior to the constraint that contains a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index of the flight plan leg.
         * @returns The index of the VNAV constraint immediately prior to the constraint that contains the specified flight
         * plan leg, or `-1` if one could nto be found.
         */
        static getPriorConstraintIndexFromLegIndex(verticalPlan, globalLegIndex) {
            for (let c = 0; c < verticalPlan.constraints.length; c++) {
                if (verticalPlan.constraints[c].index < globalLegIndex) {
                    return c;
                }
            }
            return -1;
        }
        /**
         * Gets the VNAV constraint immediately prior to the constraint that contains a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index of the flight plan leg.
         * @returns The VNAV constraint immediately prior to the constraint that contains the specified flight plan leg, or
         * `undefined` if one could nto be found.
         */
        static getPriorConstraintFromLegIndex(verticalPlan, globalLegIndex) {
            return verticalPlan.constraints[VNavUtils.getPriorConstraintIndexFromLegIndex(verticalPlan, globalLegIndex)];
        }
        /**
         * Gets and returns whether the input leg index is a path end.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex is the global leg index to check.
         * @returns whether the input leg index is a path end.
         */
        static getIsPathEnd(verticalPlan, globalLegIndex) {
            const constraintIndex = verticalPlan.constraints.findIndex(c => c.index === globalLegIndex);
            if (constraintIndex > -1 && verticalPlan.constraints[constraintIndex].isPathEnd) {
                return true;
            }
            return false;
        }
        /**
         * Gets the global leg index for the constraint containing an indexed leg.
         * @param verticalPlan The vertical plan.
         * @param globalLegIndex A global leg index.
         * @returns The global leg index for the constraint containing the leg at the specified global index, or -1 if one
         * could not be found.
         */
        static getConstraintLegIndexFromLegIndex(verticalPlan, globalLegIndex) {
            var _a, _b;
            return (_b = (_a = this.getConstraintFromLegIndex(verticalPlan, globalLegIndex)) === null || _a === void 0 ? void 0 : _a.index) !== null && _b !== void 0 ? _b : -1;
        }
        /**
         * Gets a constraint segment distance from the constraint legs.
         * @param constraint The constraint to calculate a distance for.
         * @returns The constraint distance, in meters.
         */
        static getConstraintDistanceFromConstraint(constraint) {
            let distance = 0;
            for (let legIndex = 0; legIndex < constraint.legs.length; legIndex++) {
                distance += constraint.legs[legIndex].distance;
            }
            return distance;
        }
        /**
         * Gets a constraint segment distance from the Vertical Plan legs.
         * @param constraint The constraint to calculate a distance for.
         * @param previousConstraint The constraint that preceds the constraint we are calculating the distance for.
         * @param verticalPlan The Vertical Flight Plan.
         * @returns The constraint distance, in meters.
         */
        static getConstraintDistanceFromLegs(constraint, previousConstraint, verticalPlan) {
            let distance = 0;
            const startGlobalIndex = previousConstraint !== undefined ? previousConstraint.index + 1 : 0;
            for (let i = startGlobalIndex; i <= constraint.index; i++) {
                const verticalLeg = VNavUtils.getVerticalLegFromPlan(verticalPlan, i);
                distance += verticalLeg.distance;
            }
            return distance;
        }
        /**
         * Gets the distance from the current location in the plan to the constraint.
         * @param constraint The vnav constraint to calculate the distance to.
         * @param lateralPlan The lateral flight plan.
         * @param activeLegIndex The current active leg index.
         * @param distanceAlongLeg The current distance along leg.
         * @returns the distance to the constraint, or positive infinity if a discontinuity exists between the ppos and the constraint.
         */
        static getDistanceToConstraint(constraint, lateralPlan, activeLegIndex, distanceAlongLeg) {
            if (activeLegIndex > constraint.index) {
                return 0;
            }
            let distance = 0;
            let index = activeLegIndex;
            for (const leg of lateralPlan.legs(false, activeLegIndex)) {
                if (FlightPlanUtils.isDiscontinuityLeg(leg.leg.type)) {
                    return Number.POSITIVE_INFINITY;
                }
                else if (leg.calculated !== undefined) {
                    distance += leg.calculated.distanceWithTransitions;
                }
                if (++index > constraint.index) {
                    break;
                }
            }
            distance -= distanceAlongLeg;
            return distance;
        }
        /**
         * Gets VNAV Constraint Details from a constraint.
         * @param constraint The constraint to get details from.
         * @param out The object to which write the results.
         * @returns The VNav Constraint Details.
         */
        static getConstraintDetails(constraint, out) {
            if (constraint.maxAltitude === constraint.minAltitude) {
                out.type = exports.AltitudeRestrictionType.At;
                out.altitude = Math.round(UnitType.METER.convertTo(constraint.minAltitude, UnitType.FOOT));
            }
            else if (constraint.maxAltitude < Number.POSITIVE_INFINITY || constraint.minAltitude > Number.NEGATIVE_INFINITY) {
                switch (constraint.type) {
                    case 'climb':
                    case 'missed':
                        if (constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                            out.type = exports.AltitudeRestrictionType.AtOrBelow;
                            out.altitude = Math.round(UnitType.METER.convertTo(constraint.maxAltitude, UnitType.FOOT));
                        }
                        else {
                            out.type = exports.AltitudeRestrictionType.AtOrAbove;
                            out.altitude = Math.round(UnitType.METER.convertTo(constraint.minAltitude, UnitType.FOOT));
                        }
                        break;
                    default:
                        if (constraint.minAltitude > Number.NEGATIVE_INFINITY) {
                            out.type = exports.AltitudeRestrictionType.AtOrAbove;
                            out.altitude = Math.round(UnitType.METER.convertTo(constraint.minAltitude, UnitType.FOOT));
                        }
                        else {
                            out.type = exports.AltitudeRestrictionType.AtOrBelow;
                            out.altitude = Math.round(UnitType.METER.convertTo(constraint.maxAltitude, UnitType.FOOT));
                        }
                }
            }
            else {
                out.type = exports.AltitudeRestrictionType.At;
                out.altitude = Math.round(UnitType.METER.convertTo(constraint.minAltitude, UnitType.FOOT));
            }
            return out;
        }
        /**
         * Gets and returns the vertical direct constraint based on an input index.
         * @param verticalPlan The vertical flight plan.
         * @param selectedGlobalLegIndex The global leg index selected for vertical direct.
         * @param activeLegIndex The active leg index.
         * @returns The Vnav Constraint for the vertical direct or undefined.
         */
        static getVerticalDirectConstraintFromIndex(verticalPlan, selectedGlobalLegIndex, activeLegIndex) {
            if (verticalPlan.constraints.length > 0) {
                if (selectedGlobalLegIndex < activeLegIndex) {
                    return VNavUtils.getConstraintFromLegIndex(verticalPlan, activeLegIndex);
                }
                for (let c = verticalPlan.constraints.length - 1; c >= 0; c--) {
                    const constraint = verticalPlan.constraints[c];
                    if (constraint.index === selectedGlobalLegIndex || (c === verticalPlan.constraints.length - 1 && selectedGlobalLegIndex < constraint.index)) {
                        return constraint;
                    }
                    else if (c < verticalPlan.constraints.length - 1 && constraint.index > selectedGlobalLegIndex) {
                        return verticalPlan.constraints[c + 1];
                    }
                }
            }
            return undefined;
        }
        /**
         * Gets the index of the next descent constraint at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The index of the next descent constraint at or after the specified flight plan leg, or `-1` if one could
         * not be found.
         */
        static getNextDescentConstraintIndex(verticalPlan, globalLegIndex) {
            const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
            for (let c = currentConstraintIndex; c >= 0; c--) {
                const constraint = verticalPlan.constraints[c];
                if (constraint.type !== 'climb' && constraint.type !== 'missed') {
                    return c;
                }
            }
            return -1;
        }
        /**
         * Gets the index of the next climb constraint at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The index of the next climb constraint at or after the specified flight plan leg, or `-1` if one could
         * not be found.
         */
        static getNextClimbConstraintIndex(verticalPlan, globalLegIndex) {
            const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
            for (let c = currentConstraintIndex; c >= 0; c--) {
                const constraint = verticalPlan.constraints[c];
                if (constraint.type === 'climb') {
                    return c;
                }
            }
            return -1;
        }
        /**
         * Gets the index of the next missed approach constraint at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The index of the next missed approach constraint at or after the specified flight plan leg, or `-1` if
         * one could not be found.
         */
        static getNextMaprConstraintIndex(verticalPlan, globalLegIndex) {
            const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
            for (let c = currentConstraintIndex; c >= 0; c--) {
                const constraint = verticalPlan.constraints[c];
                if (constraint.type === 'missed') {
                    return c;
                }
            }
            return -1;
        }
        /**
         * Gets the next descent constraint with a defined minimum altitude at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next descent constraint with a defined minimum altitude at or after the specified flight
         * plan leg, or `undefined` if no such constraint exists.
         */
        static getNextDescentTargetConstraint(verticalPlan, globalLegIndex) {
            const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
            for (let c = currentConstraintIndex; c >= 0; c--) {
                const constraint = verticalPlan.constraints[c];
                if ((constraint.type === 'descent' || constraint.type === 'direct' || constraint.type === 'manual') && constraint.minAltitude > Number.NEGATIVE_INFINITY) {
                    return constraint;
                }
            }
            return undefined;
        }
        /**
         * Gets the next descent constraint minimum altitude at or after a flight plan leg, or undefined if none exists.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next descent constraint defined minimum altitude in meters at or after the specified flight plan leg, or
         * `undefined` if no such constraint exists.
         */
        static getNextDescentTargetAltitude(verticalPlan, globalLegIndex) {
            const constraint = VNavUtils.getNextDescentTargetConstraint(verticalPlan, globalLegIndex);
            return constraint !== undefined ? constraint.minAltitude : undefined;
        }
        /**
         * Gets the next climb constraint with a defined maximum altitude at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next climb constraint with a defined maximum altitude at or after the specified flight plan leg, or
         * `undefined` if no such constraint exists.
         */
        static getNextClimbTargetConstraint(verticalPlan, globalLegIndex) {
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint) {
                if (currentConstraint.type === 'climb' && currentConstraint.maxAltitude < Number.POSITIVE_INFINITY) {
                    return currentConstraint;
                }
                else if (currentConstraint.type === 'climb' && currentConstraint.maxAltitude === Number.POSITIVE_INFINITY) {
                    const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
                    const lastIndexToCheck = verticalPlan.firstDescentConstraintLegIndex !== undefined ?
                        VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, verticalPlan.firstDescentConstraintLegIndex) : 0;
                    for (let c = currentConstraintIndex - 1; c >= lastIndexToCheck; c--) {
                        const constraint = verticalPlan.constraints[c];
                        if (constraint.type === 'climb' && constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                            return constraint;
                        }
                    }
                }
            }
            return undefined;
        }
        /**
         * Gets the current climb constraint, if one exists.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The current climb constraint, or `undefined` if no such constraint exists.
         */
        static getCurrentClimbConstraint(verticalPlan, globalLegIndex) {
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint && currentConstraint.type === 'climb') {
                return currentConstraint;
            }
            return undefined;
        }
        /**
         * Gets the next climb constraint maximum altitude at or after a flight plan leg, or undefined if none exists.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next climb constraint defined maximum altitude in meters at or after the specified flight plan leg, or
         * `undefined` if no such constraint exists.
         */
        static getNextClimbTargetAltitude(verticalPlan, globalLegIndex) {
            const constraint = VNavUtils.getNextClimbTargetConstraint(verticalPlan, globalLegIndex);
            return constraint !== undefined ? constraint.maxAltitude : undefined;
        }
        /**
         * Gets the next missed approach constraint with a defined maximum altitude at or after a flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next missed approach constraint with a defined maximum altitude at or after the specified flight
         * plan leg, or `undefined` if no such constraint exists.
         */
        static getNextMaprTargetConstraint(verticalPlan, globalLegIndex) {
            const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
            for (let c = currentConstraintIndex; c >= 0; c--) {
                const constraint = verticalPlan.constraints[c];
                if (constraint.type === 'missed' && constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                    return constraint;
                }
            }
            return undefined;
        }
        /**
         * Gets the next missed approach constraint maximum altitude at or after a flight plan leg, or undefined if none exists.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the flight plan leg to find the constraint for.
         * @returns The next missed approach constraint defined maximum altitude in meters at or after the specified flight plan leg, or
         * `undefined` if no such constraint exists.
         */
        static getNextMaprTargetAltitude(verticalPlan, globalLegIndex) {
            const constraint = VNavUtils.getNextMaprTargetConstraint(verticalPlan, globalLegIndex);
            return constraint !== undefined ? constraint.maxAltitude : undefined;
        }
        /**
         * Gets the VNAV desired altitude.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index to get the target for.
         * @param distanceAlongLeg The distance along the leg the aircraft is presently.
         * @returns The current VNAV desired altitude.
         */
        static getDesiredAltitude(verticalPlan, globalLegIndex, distanceAlongLeg) {
            const priorConstraint = VNavUtils.getPriorConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (priorConstraint && priorConstraint.nextVnavEligibleLegIndex && globalLegIndex < priorConstraint.nextVnavEligibleLegIndex) {
                return priorConstraint.targetAltitude;
            }
            const leg = VNavUtils.getVerticalLegFromPlan(verticalPlan, globalLegIndex);
            return leg.altitude + VNavUtils.altitudeForDistance(leg.fpa, leg.distance - distanceAlongLeg);
        }
        /**
         * Gets and returns the FAF altitude.
         * @param verticalPlan The vertical flight plan.
         * @returns the FAF constraint altitude.
         */
        static getFafAltitude(verticalPlan) {
            if (verticalPlan.fafLegIndex !== undefined) {
                return VNavUtils.getVerticalLegFromPlan(verticalPlan, verticalPlan.fafLegIndex).altitude;
            }
            return undefined;
        }
        /**
         * Gets the VNAV TOD/BOD details for a vertical flight plan.
         * @param verticalPlan The vertical flight plan.
         * @param activeLegIndex The current active leg index.
         * @param distanceAlongLeg The distance the plane is along the current leg in meters.
         * @param currentAltitude The current indicated altitude in meters.
         * @param currentVS The current vertical speed in meters per minute.
         * @param out The object to which to write the TOD/BOD details.
         * @returns The VNAV TOD/BOD details.
         */
        static getTodBodDetails(verticalPlan, activeLegIndex, distanceAlongLeg, currentAltitude, currentVS, out) {
            var _a;
            out.todLegIndex = -1;
            out.bodLegIndex = -1;
            out.todLegDistance = 0;
            out.distanceFromTod = 0;
            out.distanceFromBod = 0;
            out.currentConstraintLegIndex = -1;
            const activeConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, activeLegIndex);
            const activeConstraint = verticalPlan.constraints[activeConstraintIndex];
            // There is no TOD/BOD if...
            if (
            // ... there is no active VNAV constraint.
            !activeConstraint
                // ... the active constraint contains a VNAV-ineligible leg after the active leg.
                || ((activeConstraint === null || activeConstraint === void 0 ? void 0 : activeConstraint.nextVnavEligibleLegIndex) !== undefined && activeConstraint.nextVnavEligibleLegIndex > activeLegIndex)) {
                return out;
            }
            out.currentConstraintLegIndex = activeConstraint.index;
            // Find the next BOD, which will be at the end of the earliest non-flat descent constraint subsequent to and
            // including the active constraint that ends in a level-off at a lower altitude than the aircraft's current
            // altitude. Note that we are guaranteed to not go through a VNAV discontinuity, since all constraints that end in
            // a discontinuity also end in a level-off.
            // lag altitude by ~3 seconds so that we aren't continuously pushing TOD in front of the plane while descending.
            const altitude = currentAltitude - currentVS / 20;
            let bodConstraintIndex, bodConstraint;
            for (let i = activeConstraintIndex; i >= 0; i--) {
                const constraint = verticalPlan.constraints[i];
                // If we encounter a climb constraint, skip it.
                if (constraint.type === 'climb' || constraint.type === 'missed') {
                    continue;
                }
                if (constraint.fpa > 0 && ((_a = constraint.legs[0]) === null || _a === void 0 ? void 0 : _a.isBod) && constraint.targetAltitude <= altitude) {
                    bodConstraintIndex = i;
                    bodConstraint = constraint;
                    break;
                }
            }
            if (!bodConstraint) {
                return out;
            }
            out.bodLegIndex = bodConstraint.index;
            // Find the TOD associated with the BOD. To do this, we need to first find the earliest non-flat descent constraint
            // between the active constraint and the BOD constraint (inclusive) that is connected to the BOD constraint with no
            // intervening flat constraints or VNAV path discontinuities and whose target altitude less than the aircraft's
            // current altitude.
            let todConstraintIndex = bodConstraintIndex;
            for (let i = todConstraintIndex; i < verticalPlan.constraints.length; i++) {
                const prevConstraint = verticalPlan.constraints[i + 1];
                if (!prevConstraint
                    || prevConstraint.index < activeLegIndex
                    || prevConstraint.type === 'climb'
                    || prevConstraint.type === 'missed'
                    || prevConstraint.targetAltitude > altitude
                    || prevConstraint.fpa <= 0
                    || prevConstraint.isPathEnd) {
                    todConstraintIndex = i;
                    break;
                }
            }
            const todConstraint = verticalPlan.constraints[todConstraintIndex];
            // Now that we have found the TOD constraint, we need to find the TOD leg: the leg on which the TOD actually lies.
            // To do this, we calculate the along-track distance from the end of the TOD constraint to the TOD, then iterate
            // through the legs in the constraint backwards while keeping track of the total along-track distance covered by
            // each leg.
            let distance = VNavUtils.distanceForAltitude(todConstraint.fpa, altitude - todConstraint.targetAltitude);
            let constraintIndex = todConstraintIndex;
            let todLegIndex = todConstraint.index;
            let todLegDistance = 0;
            let todLeg = todConstraint.legs[0];
            while (distance > 0 && constraintIndex < verticalPlan.constraints.length) {
                const constraint = verticalPlan.constraints[constraintIndex];
                // Remember that flight plan legs in a VNAV constraint appear in reverse order relative to how they are ordered
                // in the flight plan.
                for (let i = 0; i < constraint.legs.length; i++) {
                    if (!constraint.legs[i].isEligible) {
                        // We've encounted a VNAV-ineligible leg. Since we cannot calculate a vertical path through this leg, we have
                        // to stop iterating now so that the TOD gets set to the most recent VNAV-eligible leg.
                        constraintIndex = verticalPlan.constraints.length;
                        break;
                    }
                    todLeg = constraint.legs[i];
                    distance -= todLeg.distance;
                    if (distance <= 0) {
                        todLegIndex = constraint.index - i;
                        todLegDistance = todLeg.distance + distance;
                        break;
                    }
                }
                constraintIndex++;
            }
            if (distance > 0) {
                // If we still haven't found the TOD yet, set it to the beginning of the earliest VNAV leg that was iterated.
                todLegIndex = verticalPlan.segments[todLeg.segmentIndex].offset + todLeg.legIndex;
                todLegDistance = todLeg.distance;
            }
            out.todLegIndex = todLegIndex;
            out.todLegDistance = todLegDistance;
            // calculate distance to TOD/BOD
            let globalLegIndex = bodConstraint.index;
            let distanceToBOD = 0, distanceToTOD = 0;
            let hasReachedTOD = false;
            let isDone = false;
            for (let i = bodConstraintIndex; i < verticalPlan.constraints.length; i++) {
                const constraint = verticalPlan.constraints[i];
                for (let j = 0; j < constraint.legs.length; j++) {
                    const leg = constraint.legs[j];
                    if (globalLegIndex === todLegIndex) {
                        distanceToTOD -= todLegDistance;
                        hasReachedTOD = true;
                    }
                    if (globalLegIndex > activeLegIndex) {
                        distanceToBOD += leg.distance;
                        if (hasReachedTOD) {
                            distanceToTOD += leg.distance;
                        }
                    }
                    else if (globalLegIndex === activeLegIndex) {
                        distanceToBOD += leg.distance - distanceAlongLeg;
                        if (hasReachedTOD) {
                            distanceToTOD += leg.distance - distanceAlongLeg;
                            isDone = true;
                        }
                        else {
                            distanceToTOD -= distanceAlongLeg;
                        }
                    }
                    else {
                        if (hasReachedTOD) {
                            isDone = true;
                        }
                        else {
                            distanceToTOD -= leg.distance;
                        }
                    }
                    if (isDone) {
                        break;
                    }
                    else {
                        globalLegIndex--;
                    }
                }
                if (isDone) {
                    break;
                }
            }
            out.distanceFromBod = distanceToBOD;
            out.distanceFromTod = distanceToTOD;
            return out;
        }
        /**
         * Gets the VNAV TOC/BOC details for a vertical flight plan.
         * @param verticalPlan The vertical flight plan.
         * @param activeLegIndex The current active leg index.
         * @param distanceAlongLeg The distance the plane is along the current leg in meters.
         * @param currentGroundSpeed The current ground speed, in knots.
         * @param currentAltitude The current indicated altitude in meters.
         * @param currentVS The current vertical speed in meters per minute.
         * @param out The object to which to write the TOC/BOC details.
         * @returns The VNAV TOC/BOC details.
         */
        static getTocBocDetails(verticalPlan, activeLegIndex, distanceAlongLeg, currentGroundSpeed, currentAltitude, currentVS, out) {
            var _a, _b;
            out.bocLegIndex = -1;
            out.tocLegIndex = -1;
            out.tocLegDistance = 0;
            out.distanceFromBoc = 0;
            out.distanceFromToc = 0;
            out.tocConstraintIndex = -1;
            out.tocAltitude = -1;
            const activeConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, activeLegIndex);
            const activeConstraint = verticalPlan.constraints[activeConstraintIndex];
            // There is no BOC/TOC if...
            if (
            // ... there is no active VNAV constraint.
            !activeConstraint
                // ... the active VNAV constraint is not a climb-type constraint.
                || (activeConstraint.type !== 'climb' && activeConstraint.type !== 'missed')) {
                return out;
            }
            // Find the TOC. To do this, we need to first find the earliest climb constraint subsequent to and including the
            // active constraint that has a maximum altitude (i.e. is an AT, AT OR BELOW, or BETWEEN constraint). Additionally,
            // the TOC must not be separated from the active constraint by a descent-type constraint.
            let tocConstraintIndex, tocConstraint;
            for (let i = activeConstraintIndex; i >= 0; i--) {
                const constraint = verticalPlan.constraints[i];
                // If we encounter a descent constraint, immediately terminate the search.
                if (constraint.type !== 'climb' && constraint.type !== 'missed') {
                    break;
                }
                if (isFinite(constraint.maxAltitude)) {
                    tocConstraintIndex = i;
                    tocConstraint = constraint;
                    break;
                }
            }
            // If there is no next TOC, there also can be no next BOC since the next BOC must follow the next TOC.
            if (!tocConstraint) {
                return out;
            }
            out.tocConstraintIndex = tocConstraintIndex;
            out.tocAltitude = tocConstraint.maxAltitude;
            // Calculate distance to TOC.
            const deltaAltitude = tocConstraint.maxAltitude - currentAltitude;
            const timeToTocMin = deltaAltitude / Math.max(0, currentVS);
            let distanceRemaining = currentGroundSpeed === 0 ? 0 : timeToTocMin * UnitType.KNOT.convertTo(currentGroundSpeed, UnitType.MPM);
            // Find the leg on which the TOC lies.
            const activeLeg = activeConstraint.legs[activeConstraint.index - activeLegIndex];
            let tocLegIndex;
            let currentConstraintIndex = activeConstraintIndex;
            let currentConstraint;
            let currentConstraintLegIndex = activeConstraint.index - activeLegIndex;
            let currentLeg = activeLeg;
            const activeLegDistanceRemaining = ((_a = activeLeg === null || activeLeg === void 0 ? void 0 : activeLeg.distance) !== null && _a !== void 0 ? _a : 0) - distanceAlongLeg;
            if (distanceRemaining > activeLegDistanceRemaining) {
                distanceRemaining -= activeLegDistanceRemaining;
                if (currentConstraintLegIndex <= 0) {
                    --currentConstraintIndex;
                }
                else {
                    currentLeg = activeConstraint.legs[--currentConstraintLegIndex];
                }
                while (currentConstraintIndex >= tocConstraintIndex) {
                    currentConstraint = verticalPlan.constraints[currentConstraintIndex];
                    currentLeg = currentConstraint.legs[currentConstraintLegIndex];
                    if (currentLeg !== undefined) {
                        if (distanceRemaining > currentLeg.distance) {
                            out.distanceFromToc += currentLeg.distance;
                            distanceRemaining -= currentLeg.distance;
                        }
                        else {
                            out.distanceFromToc += distanceRemaining;
                            tocLegIndex = currentConstraint.index - currentConstraintLegIndex;
                            distanceRemaining -= currentLeg.distance;
                            break;
                        }
                    }
                    if (currentConstraintLegIndex <= 0) {
                        --currentConstraintIndex;
                    }
                    else {
                        currentLeg = currentConstraint.legs[--currentConstraintLegIndex];
                    }
                }
            }
            else {
                out.distanceFromToc = distanceRemaining;
                tocLegIndex = activeLegIndex;
                distanceRemaining -= activeLegDistanceRemaining;
            }
            if (tocLegIndex === undefined) {
                // If we still haven't found the TOC yet, set it to the end of the last leg of the TOC constraint.
                out.tocLegIndex = tocConstraint.index;
                out.tocLegDistance = 0;
            }
            else {
                out.tocLegIndex = tocLegIndex;
                out.tocLegDistance = -distanceRemaining;
            }
            // Find the next BOC, which is located at the beginning of the earliest climb constraint subsequent to (and not
            // including) the TOC constraint with a maximum altitude greater than the TOC constraint. Additionally, the BOC
            // must not be separated from the TOC constraint by a descent-type constraint.
            let lastClimbConstraintIndex = tocConstraintIndex;
            let bocConstraintIndex, bocConstraint;
            for (let i = tocConstraintIndex - 1; i >= 0; i--) {
                const constraint = verticalPlan.constraints[i];
                // If we encounter a descent constraint, immediately terminate the search.
                if (constraint.type !== 'climb' && constraint.type !== 'missed') {
                    break;
                }
                if (constraint.maxAltitude > tocConstraint.maxAltitude) {
                    bocConstraintIndex = i;
                    bocConstraint = constraint;
                    break;
                }
                lastClimbConstraintIndex = i;
            }
            let bocDistanceStopConstraintIndex = undefined;
            if (bocConstraint) {
                out.bocLegIndex = bocConstraint.index - (bocConstraint.legs.length - 1);
                bocDistanceStopConstraintIndex = bocConstraintIndex;
            }
            else {
                // If we did not find a climb constraint subsequent to the TOC constraint with a maximum altitude greater than the
                // the TOC constraint, then the BOC will be located at the last climb constraint.
                const lastClimbConstraint = verticalPlan.constraints[lastClimbConstraintIndex];
                if (lastClimbConstraint && lastClimbConstraint.index + 1 < verticalPlan.length) {
                    out.bocLegIndex = lastClimbConstraint.index + 1;
                    bocDistanceStopConstraintIndex = lastClimbConstraintIndex - 1;
                }
            }
            // Calculate distance to BOC
            if (bocDistanceStopConstraintIndex !== undefined) {
                let distanceToEndOfActiveConstraint = ((_b = activeLeg === null || activeLeg === void 0 ? void 0 : activeLeg.distance) !== null && _b !== void 0 ? _b : 0) - distanceAlongLeg;
                for (let i = Math.min(activeConstraint.index - activeLegIndex, activeConstraint.legs.length) - 1; i >= 0; i--) {
                    distanceToEndOfActiveConstraint += activeConstraint.legs[i].distance;
                }
                out.distanceFromBoc = distanceToEndOfActiveConstraint;
                for (let i = activeConstraintIndex - 1; i > bocDistanceStopConstraintIndex; i--) {
                    out.distanceFromBoc += verticalPlan.constraints[i].distance;
                }
            }
            return out;
        }
        /**
         * Gets the VNAV TOC/BOC to cruise altitude details for a vertical flight plan.
         * @param lateralPlan The lateral flight plan.
         * @param verticalPlan The vertical flight plan.
         * @param activeLegIndex The current active leg index.
         * @param distanceAlongLeg The distance the plane is along the current leg in meters.
         * @param currentGroundSpeed The current ground speed, in knots.
         * @param currentAltitude The current indicated altitude in meters.
         * @param currentVS The current vertical speed in meters per minute.
         * @param cruiseAltitude The cruise altitude, in meters.
         * @param out The object to which to write the TOC/BOC details.
         * @returns The VNAV TOC/BOC to cruise altitude details.
         */
        static getCruiseTocBocDetails(lateralPlan, verticalPlan, activeLegIndex, distanceAlongLeg, currentGroundSpeed, currentAltitude, currentVS, cruiseAltitude, out) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j;
            out.bocLegIndex = -1;
            out.tocLegIndex = -1;
            out.tocLegDistance = 0;
            out.distanceFromBoc = 0;
            out.distanceFromToc = 0;
            out.tocConstraintIndex = -1;
            out.tocAltitude = -1;
            // Find the last climb constraint
            const lastClimbConstraintIndex = VNavUtils.getLastClimbConstraintIndex(verticalPlan);
            const lastClimbConstraint = verticalPlan.constraints[lastClimbConstraintIndex];
            const firstDescentConstraintIndex = VNavUtils.getFirstDescentConstraintIndex(verticalPlan);
            const firstDescentConstraint = verticalPlan.constraints[firstDescentConstraintIndex];
            // If the active leg is past the first descent constraint, both cruise BOC and cruise TOC are undefined.
            if (firstDescentConstraint && activeLegIndex > firstDescentConstraint.index) {
                return out;
            }
            const activeLeg = lateralPlan.tryGetLeg(activeLegIndex);
            const activeLegDistanceRemaining = ((_b = (_a = activeLeg === null || activeLeg === void 0 ? void 0 : activeLeg.calculated) === null || _a === void 0 ? void 0 : _a.distanceWithTransitions) !== null && _b !== void 0 ? _b : 0) - distanceAlongLeg;
            // Cruise BOC will always be located at the beginning of the first leg after the last climb constraint. If there
            // are no climb constraints in the plan, then cruise BOC is undefined.
            if (lastClimbConstraint && lastClimbConstraint.index < lateralPlan.length - 1 && activeLegIndex <= lastClimbConstraint.index) {
                const lastClimbConstraintLeg = lateralPlan.tryGetLeg(lastClimbConstraint.index);
                out.bocLegIndex = lastClimbConstraint.index + 1;
                out.distanceFromBoc = activeLegDistanceRemaining
                    + ((_d = (_c = lastClimbConstraintLeg === null || lastClimbConstraintLeg === void 0 ? void 0 : lastClimbConstraintLeg.calculated) === null || _c === void 0 ? void 0 : _c.cumulativeDistanceWithTransitions) !== null && _d !== void 0 ? _d : 0) - ((_f = (_e = activeLeg === null || activeLeg === void 0 ? void 0 : activeLeg.calculated) === null || _e === void 0 ? void 0 : _e.cumulativeDistanceWithTransitions) !== null && _f !== void 0 ? _f : 0);
            }
            // Calculate distance to TOC.
            const deltaAltitude = cruiseAltitude - currentAltitude;
            const timeToTocMin = deltaAltitude / Math.max(0, currentVS);
            let distanceRemaining = currentGroundSpeed === 0 ? 0 : timeToTocMin * UnitType.KNOT.convertTo(currentGroundSpeed, UnitType.MPM);
            // Find the leg on which the TOC lies. The TOC is restricted to legs prior to the first descent constraint.
            let tocLegIndex;
            const lastLegIndex = (_g = firstDescentConstraint === null || firstDescentConstraint === void 0 ? void 0 : firstDescentConstraint.index) !== null && _g !== void 0 ? _g : lateralPlan.length - 1;
            if (distanceRemaining > activeLegDistanceRemaining) {
                let legIndex = activeLegIndex + 1;
                for (const leg of lateralPlan.legs(false, legIndex, lastLegIndex + 1)) {
                    const legDistance = (_j = (_h = leg.calculated) === null || _h === void 0 ? void 0 : _h.distanceWithTransitions) !== null && _j !== void 0 ? _j : 0;
                    if (distanceRemaining > legDistance) {
                        out.distanceFromToc += legDistance;
                        distanceRemaining -= legDistance;
                    }
                    else {
                        out.distanceFromToc += distanceRemaining;
                        tocLegIndex = legIndex;
                        distanceRemaining -= legDistance;
                        break;
                    }
                    legIndex++;
                }
            }
            else {
                out.distanceFromToc = distanceRemaining;
                tocLegIndex = activeLegIndex;
                distanceRemaining -= activeLegDistanceRemaining;
            }
            if (tocLegIndex === undefined) {
                // If we still haven't found the TOC yet, set it to the end of the last viable leg.
                out.tocLegIndex = lastLegIndex;
                out.tocLegDistance = 0;
            }
            else {
                out.tocLegIndex = tocLegIndex;
                out.tocLegDistance = -distanceRemaining;
            }
            out.tocAltitude = cruiseAltitude;
            return out;
        }
        /**
         * Checks whether or not the vertical plan has a leg at a given globalLegIndex.
         * @param verticalPlan The Vertical Flight Plan.
         * @param globalLegIndex The global leg index to check.
         * @returns True if the leg exists.
         */
        static verticalPlanHasLeg(verticalPlan, globalLegIndex) {
            for (let i = 0; i < verticalPlan.segments.length; i++) {
                const segment = verticalPlan.segments[i];
                if (segment !== undefined && globalLegIndex >= segment.offset && globalLegIndex < segment.offset + segment.legs.length) {
                    return segment.legs[globalLegIndex - segment.offset] !== undefined;
                }
            }
            return false;
        }
        /**
         * Gets a VNAV leg from a vertical flight plan.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global leg index of the leg to get.
         * @returns The requested VNAV leg.
         * @throws Not found if the index is not valid.
         */
        static getVerticalLegFromPlan(verticalPlan, globalLegIndex) {
            for (let i = 0; i < verticalPlan.segments.length; i++) {
                const segment = verticalPlan.segments[i];
                if (segment !== undefined && globalLegIndex >= segment.offset && globalLegIndex < segment.offset + segment.legs.length) {
                    return segment.legs[globalLegIndex - segment.offset];
                }
            }
            throw new Error(`Leg with index ${globalLegIndex} not found`);
        }
        /**
         * Gets a VNAV leg from the plan from a specified segment.
         * @param verticalPlan The vertical flight plan.
         * @param segmentIndex The segment index of the leg to get.
         * @param legIndex The index of the leg to get within the specified segment.
         * @returns The requested VNAV leg.
         * @throws Not found if the index is not valid.
         */
        static getVerticalLegFromSegmentInPlan(verticalPlan, segmentIndex, legIndex) {
            const segment = verticalPlan.segments[segmentIndex];
            const leg = segment.legs[legIndex];
            if (segment && leg) {
                return leg;
            }
            else {
                throw new Error(`Leg from vertical plan ${verticalPlan.planIndex} segment ${segmentIndex} index ${legIndex} not found`);
            }
        }
        /**
         * Gets the constraint for a vertical direct based on an input global leg index.
         * @param verticalPlan The vertical flight plan.
         * @param activeGlobalLegIndex The current active global leg index.
         * @param selectedGlobalLegIndex The input global leg index selected.
         * @returns The constraint, or undefined if none exists.
         */
        static getConstraintForVerticalDirect(verticalPlan, activeGlobalLegIndex, selectedGlobalLegIndex) {
            return VNavUtils.getVerticalDirectConstraintFromIndex(verticalPlan, selectedGlobalLegIndex, activeGlobalLegIndex);
        }
        /**
         * Gets the VNAV segments from the calculated VNAV plan.
         * @param verticalPlan The vertical flight plan.
         * @returns The vnav segments.
         * @throws Not found if the index is not valid.
         */
        static getVerticalSegmentsFromPlan(verticalPlan) {
            return verticalPlan.segments;
        }
        /**
         * Gets whether a lateral plan leg is a hold or procedure turn.
         * @param lateralLeg The Lateral Leg in the flight plan (LegDefinition).
         * @returns Whether the leg is a hold or procedure turn.
         */
        static isLegTypeHoldOrProcedureTurn(lateralLeg) {
            if (lateralLeg.leg !== undefined) {
                switch (lateralLeg.leg.type) {
                    case exports.LegType.HA:
                    case exports.LegType.HF:
                    case exports.LegType.HM:
                    case exports.LegType.PI:
                        return true;
                }
            }
            return false;
        }
        /**
         * Creates a new empty vertical flight plan constraint.
         * @param index The leg index of the constraint.
         * @param minAltitude The bottom altitude of the constraint.
         * @param maxAltitude THe top altitude of the constraint.
         * @param name The name of the leg for the constraint.
         * @param type The type of constraint.
         * @returns A new empty constraint.
         */
        static createConstraint(index, minAltitude, maxAltitude, name, type = 'descent') {
            return {
                index,
                minAltitude,
                maxAltitude,
                targetAltitude: 0,
                name,
                isTarget: false,
                isPathEnd: false,
                distance: 0,
                fpa: 0,
                legs: [],
                type,
                isBeyondFaf: false
            };
        }
        /**
         * Creates a new vertical flight plan leg.
         * @param segmentIndex The segment index for the leg.
         * @param legIndex The index of the leg within the segment.
         * @param name The name of the leg.
         * @param distance The leg distance.
         * @returns A new VNAV plan leg.
         */
        static createLeg(segmentIndex, legIndex, name, distance = 0) {
            return {
                segmentIndex,
                legIndex,
                fpa: 0,
                altitude: 0,
                isUserDefined: false,
                isDirectToTarget: false,
                distance: distance,
                isEligible: true,
                isBod: false,
                isAdvisory: true,
                name
            };
        }
        /**
         * Finds the index of the first climb constraint in a vertical plan.
         * @param verticalPlan A vertical flight plan.
         * @returns The index of the first climb constraint in the specified vertical plan, or `-1` if the plan has no
         * climb constraints.
         */
        static getFirstClimbConstraintIndex(verticalPlan) {
            for (let i = verticalPlan.constraints.length - 1; i >= 0; i--) {
                if (verticalPlan.constraints[i].type === 'climb') {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Finds the index of the last climb constraint in a vertical plan.
         * @param verticalPlan A vertical flight plan.
         * @returns The index of the last climb constraint in the specified vertical plan, or `-1` if the plan has no
         * climb constraints.
         */
        static getLastClimbConstraintIndex(verticalPlan) {
            for (let i = 0; i < verticalPlan.constraints.length; i++) {
                if (verticalPlan.constraints[i].type === 'climb') {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Finds the index of the first descent constraint in a vertical plan.
         * @param verticalPlan A vertical flight plan.
         * @returns The index of the first descent constraint in the specified vertical plan, or `-1` if the plan has no
         * descent constraints.
         */
        static getFirstDescentConstraintIndex(verticalPlan) {
            let index = -1;
            for (let c = 0; c < verticalPlan.constraints.length; c++) {
                const type = verticalPlan.constraints[c].type;
                if (type === 'descent' || type === 'manual') {
                    index = c;
                }
                if (type === 'direct') {
                    return c;
                }
            }
            return index;
        }
        /**
         * Finds the index of the last descent constraint in a vertical plan.
         * @param verticalPlan A vertical flight plan.
         * @returns The index of the last descent constraint in the specified vertical plan, or `-1` if the plan has no
         * descent constraints.
         */
        static getLastDescentConstraintIndex(verticalPlan) {
            for (let i = 0; i < verticalPlan.constraints.length; i++) {
                const type = verticalPlan.constraints[i].type;
                if (type === 'descent' || type === 'direct' || type === 'manual') {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Checks whether two speed constraints are equal.
         * @param a The first speed constraint.
         * @param b The second speed constraint.
         * @returns Whether the two speed constraints are equal.
         */
        static speedConstraintEquals(a, b) {
            return a.speedDesc === b.speedDesc && a.speed === b.speed && a.speedUnit === b.speedUnit;
        }
        /**
         * Checks whether two altitude constraint details are equal.
         * @param a The first altitude constraint details.
         * @param b The second altitude constraint details.
         * @returns Whether the two altitude constraint details are equal.
         */
        static altitudeConstraintDetailsEquals(a, b) {
            return a.type === b.type && a.altitude === b.altitude;
        }
        /**
         * Checks whether two speed constraint details are equal.
         * @param a The first speed constraint details.
         * @param b The second speed constraint details.
         * @returns Whether the two speed constraint details are equal.
         */
        static speedConstraintDetailsEquals(a, b) {
            return a.distanceToNextSpeedConstraint === b.distanceToNextSpeedConstraint
                && VNavUtils.speedConstraintEquals(a.currentSpeedConstraint, b.currentSpeedConstraint)
                && VNavUtils.speedConstraintEquals(a.nextSpeedConstraint, b.nextSpeedConstraint);
        }
        /**
         * Computes the path error distance that should be used given the groundspeed.
         * @param groundSpeed The current groundspeed, in knots.
         * @returns The path error distance to use.
         */
        static getPathErrorDistance(groundSpeed) {
            if (groundSpeed <= 190) {
                return 100;
            }
            else if (groundSpeed >= 210) {
                return 250;
            }
            else {
                return 100 + (((groundSpeed - 190) / 20) * 150);
            }
        }
        /**
         * Checks whether an index is a valid VNAV index.
         * @param index The index to check.
         * @returns Whether the specified index is a valid VNAV index.
         */
        static isValidVNavIndex(index) {
            return Number.isInteger(index) && index >= 0;
        }
        /**
         * Gets the suffix for event bus topics published by VNAV with a given index.
         * @param index The index of the VNAV for which to get the suffix.
         * @returns The suffix for event bus topics published by VNAV with the specified index.
         */
        static getEventBusTopicSuffix(index) {
            return (index === 0 ? '' : `_${index}`);
        }
    }

    /**
     * The state of a given plane director.
     */
    exports.DirectorState = void 0;
    (function (DirectorState) {
        /** The plane director is not currently armed or active. */
        DirectorState["Inactive"] = "Inactive";
        /** The plane director is currently armed. */
        DirectorState["Armed"] = "Armed";
        /** The plane director is currently active. */
        DirectorState["Active"] = "Active";
    })(exports.DirectorState || (exports.DirectorState = {}));
    /**
     * A plane director that provides no behavior.
     */
    class EmptyDirector {
        constructor() {
            /** No-op. */
            this.onActivate = () => { };
            /** No-op */
            this.onArm = () => { };
            this.state = exports.DirectorState.Inactive;
        }
        /** No-op. */
        activate() { }
        /** No-op. */
        deactivate() { }
        /** No-op. */
        update() { }
        /** No-op. */
        arm() { }
    }
    /** An instance of the empty plane director. */
    EmptyDirector.instance = new EmptyDirector();

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * An altitude capture autopilot director.
     */
    class APAltCapDirector {
        /**
         * Creates an instance of the APAltCapDirector.
         * @param apValues Autopilot data for this director.
         * @param options Optional options object with these:
         * --> shouldActivate: An optional function which returns true if the capturing shall be activated. If not
         * defined, a default function is used.
         * --> captureAltitude: An optional function which calculates desired pitch angles to capture a target altitude. If not
         * defined, a default function is used.
         */
        constructor(apValues, options) {
            this.apValues = apValues;
            this.initialFpa = 0;
            this.captureAltitude = APAltCapDirector.captureAltitude;
            this.shouldActivate = APAltCapDirector.shouldActivate;
            this.state = exports.DirectorState.Inactive;
            if ((options === null || options === void 0 ? void 0 : options.captureAltitude) !== undefined) {
                this.captureAltitude = options.captureAltitude;
            }
            if ((options === null || options === void 0 ? void 0 : options.shouldActivate) !== undefined) {
                this.shouldActivate = options.shouldActivate;
            }
        }
        /**
         * Activates this director.
         * @param vs Optionally, the current vertical speed, in FPM.
         * @param alt Optionally, the current indicated altitude, in Feet.
         */
        activate(vs, alt) {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            this.setCaptureFpa(vs !== undefined ? vs : SimVar.GetSimVarValue('VERTICAL SPEED', exports.SimVarValueType.FPM), alt !== undefined ? alt : SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet));
            SimVar.SetSimVarValue('AUTOPILOT ALTITUDE LOCK', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            this.state = exports.DirectorState.Armed;
            if (this.onArm !== undefined) {
                this.onArm();
            }
        }
        /**
         * Deactivates this director.
         * @param captured is whether the altitude was captured.
         */
        deactivate(captured = false) {
            this.state = exports.DirectorState.Inactive;
            if (!captured) {
                SimVar.SetSimVarValue('AUTOPILOT ALTITUDE LOCK', 'Bool', false);
            }
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.drivePitch && this.drivePitch(-this.captureAltitude(this.apValues.capturedAltitude.get(), SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet), this.initialFpa, SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.Knots)), true, true);
            }
            else if (this.state === exports.DirectorState.Armed) {
                this.tryActivate();
            }
        }
        /**
         * Attempts to activate altitude capture.
         */
        tryActivate() {
            const selectedAltitude = this.apValues.selectedAltitude.get();
            const vs = SimVar.GetSimVarValue('VERTICAL SPEED', exports.SimVarValueType.FPM);
            const alt = SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet);
            if (this.shouldActivate(vs, selectedAltitude, alt)) {
                this.apValues.capturedAltitude.set(Math.round(selectedAltitude));
                this.activate(vs, alt);
            }
        }
        /**
         * A function which returns true if the capturing shall be activated
         * @param vs Current vertical speed in [ft/min]
         * @param targetAltitude Target altitude [ft]
         * @param currentAltitude Current altitude [ft]
         * @returns True if the capturing shall be activated
         */
        static shouldActivate(vs, targetAltitude, currentAltitude) {
            return (Math.abs(targetAltitude - currentAltitude) <= Math.abs(vs / 6));
        }
        /**
         * Sets the initial capture FPA from the current vs value when capture is initiated.
         * @param vs The current vertical speed, in FPM.
         * @param alt The current indicated altitude, in Feet.
         */
        setCaptureFpa(vs, alt) {
            const altCapDeviation = alt - this.apValues.selectedAltitude.get();
            if (altCapDeviation < 0) {
                vs = Math.max(400, vs);
            }
            else {
                vs = Math.min(-400, vs);
            }
            const tas = SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.FPM);
            this.initialFpa = VNavUtils.getFpa(tas, vs);
        }
        /**
         * Calculates a desired pitch angle, in degrees, to capture a target altitude.
         * @param targetAltitude The altitude to capture, in feet.
         * @param indicatedAltitude The current indicated altitude, in feet.
         * @param initialFpa The flight path angle of the airplane, in degrees, when altitude capture was first activated.
         * Positive values indicate a descending path.
         * @param tas The current true airspeed of the airplane, in knots.
         * @returns The desired pitch angle, in degrees, to capture the specified altitude. Positive values indicate nose-up
         * pitch.
         */
        static captureAltitude(targetAltitude, indicatedAltitude, initialFpa, tas) {
            const initialFpaAbs = Math.abs(initialFpa);
            let deltaAltitude = targetAltitude - indicatedAltitude;
            if (deltaAltitude >= 0 && deltaAltitude < 10) {
                deltaAltitude = 10;
            }
            else if (deltaAltitude < 0 && deltaAltitude > -10) {
                deltaAltitude = -10;
            }
            const desiredClosureTime = MathUtils.lerp(Math.abs(deltaAltitude), 100, 1000, 5, 10, true, true);
            const desiredVs = deltaAltitude / (desiredClosureTime / 60);
            const desiredFpa = MathUtils.clamp(Math.asin(desiredVs / UnitType.KNOT.convertTo(tas, UnitType.FPM)) * Avionics.Utils.RAD2DEG, -initialFpaAbs, initialFpaAbs);
            return MathUtils.clamp(desiredFpa, -15, 15);
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * An altitude hold autopilot director.
     */
    class APAltDirector {
        /**
         * Creates an instance of the LateralDirector.
         * @param apValues are the ap selected values for the autopilot.
         */
        constructor(apValues) {
            this.capturedAltitude = 0;
            this.setCapturedAltitude = (alt) => {
                this.capturedAltitude = Math.round(alt);
            };
            this.state = exports.DirectorState.Inactive;
            this.capturedAltitudeSub = apValues.capturedAltitude.sub(this.setCapturedAltitude, true);
            this.pauseSubs();
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.capturedAltitudeSub.resume(true);
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.capturedAltitudeSub.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT ALTITUDE LOCK', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            this.resumeSubs();
            this.state = exports.DirectorState.Armed;
            if (this.onArm !== undefined) {
                this.onArm();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT ALTITUDE LOCK', 'Bool', false);
            this.pauseSubs();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.holdAltitude(this.capturedAltitude);
            }
            if (this.state === exports.DirectorState.Armed) {
                this.tryActivate();
            }
        }
        /**
         * Attempts to activate altitude capture.
         */
        tryActivate() {
            const deviationFromTarget = Math.abs(this.capturedAltitude - SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet));
            if (deviationFromTarget <= 20) {
                this.activate();
            }
        }
        /**
         * Holds a captured altitude.
         * @param targetAltitude is the captured targed altitude
         */
        holdAltitude(targetAltitude) {
            const deltaAlt = SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet) - targetAltitude;
            let setVerticalSpeed = 0;
            const correction = MathUtils.clamp(10 * Math.abs(deltaAlt), 100, 500);
            if (deltaAlt > 10) {
                setVerticalSpeed = 0 - correction;
            }
            else if (deltaAlt < -10) {
                setVerticalSpeed = correction;
            }
            this.drivePitch && this.drivePitch(this.getDesiredPitch(setVerticalSpeed), true, true);
        }
        /**
         * Gets a desired pitch from the selected vs value.
         * @param vs target vertical speed.
         * @returns The desired pitch angle.
         */
        getDesiredPitch(vs) {
            const desiredPitch = VNavUtils.getFpa(SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.FPM), vs);
            return -NavMath.clamp(desiredPitch, -10, 10);
        }
    }

    /** A PID controller. */
    class PidController {
        /**
         * Creates a new PidController.
         * @param kP The proportional gain of the controller.
         * @param kI The integral gain of the controller.
         * @param kD The differential gain of the controller.
         * @param maxOut The maximum output of the controller.
         * @param minOut The minumum output of the controller.
         * @param maxI The maximum integral gain.
         * @param minI The minimum integral gain.
         */
        constructor(kP, kI, kD, maxOut, minOut, maxI = Number.MAX_SAFE_INTEGER, minI = Number.MIN_SAFE_INTEGER) {
            this.kP = kP;
            this.kI = kI;
            this.kD = kD;
            this.maxOut = maxOut;
            this.minOut = minOut;
            this.maxI = maxI;
            this.minI = minI;
            /** The previously sampled error. */
            this.previousError = undefined;
            /** The previously generated output. */
            this.previousOutput = undefined;
            /** The currently accumulated integral. */
            this.integral = 0;
        }
        /**
         * Gets this controller's most recent error input since it was created or reset.
         * @returns This controller's most recent error input since it was created or reset.
         */
        getPreviousError() {
            return this.previousError;
        }
        /**
         * Gets the output of the PID controller at a given time.
         * @param deltaTime The difference in time between the previous sample and this sample.
         * @param error The amount of error seen between the desired output and the current output.
         * @returns The PID output.
         */
        getOutput(deltaTime, error) {
            var _a;
            const p = this.kP * error;
            if (this.previousError !== undefined && Math.sign(error) === Math.sign(this.previousError)) {
                this.integral += ((error * deltaTime) + ((deltaTime * (error - this.previousError)) / 2)) * this.kI;
                this.integral = PidController.clamp(this.integral, this.maxI, this.minI);
            }
            else {
                this.integral = 0;
            }
            const i = this.integral;
            const d = deltaTime === 0 ? 0 : this.kD * ((error - ((_a = this.previousError) !== null && _a !== void 0 ? _a : error)) / deltaTime);
            const output = PidController.clamp(p + i + d, this.maxOut, this.minOut);
            this.previousError = error;
            this.previousOutput = output;
            return output;
        }
        /** Resets the controller. */
        reset() {
            this.previousError = undefined;
            this.previousOutput = undefined;
            this.integral = 0;
        }
        /**
         * Clamps a number to maximum and minimum values.
         * @param value The value to clamp.
         * @param max The maximum value.
         * @param min The minumum value.
         * @returns The clamped value.
         */
        static clamp(value, max, min) {
            return Math.min(Math.max(value, min), max);
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A Generic FLC computer to be used in directors that require FLC logic.
     */
    class GenericFlcComputer {
        /**
         * Gets if this computer is active
         * @returns if this computer is active.
         */
        get isActive() {
            return this._isActive;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** This computer's target speed, in knots indicated airspeed. */
        get targetIas() {
            return this._targetIas;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * Whether this computer is in climb mode. In climb mode, the computer will not target a pitch that would cause the
         * airplane to descend. When not in climb mode, the computer will not target a pitch that would cause the airplane to
         * climb.
         */
        get isClimbMode() {
            return this._climbMode;
        }
        /**
         * Creates an instance of GenericFlcComputer.
         * @param pidControllerOptions The PID controller settings for this computer.
         */
        constructor(pidControllerOptions) {
            this._isActive = false;
            this._targetIas = 0;
            this._climbMode = false;
            this._pitchTarget = Subject.create(null);
            /** The current pitch target calculated by this computer, in degrees. Positive values indicate downward pitch. */
            this.pitchTarget = this._pitchTarget;
            this._lastTime = 0;
            this.filter = new ExpSmoother(2.5);
            // this.pitchController = new PidController(2, 0, 0, 15, -15);
            this.pitchController = new PidController(pidControllerOptions.kP, pidControllerOptions.kI, pidControllerOptions.kD, pidControllerOptions.maxOut, pidControllerOptions.minOut, pidControllerOptions.maxI, pidControllerOptions.minI);
        }
        /**
         * Activates this computer.
         * @param climbMode Whether to force climb mode on (`true`) or off (`false`) on activation. If undefined, the climb
         * mode state will remain unchanged.
         */
        activate(climbMode) {
            this._isActive = true;
            if (climbMode !== undefined) {
                this._climbMode = climbMode;
            }
            this.initialize();
        }
        /**
         * Turns climb mode on or off.
         * @param setToClimbMode Whether climb mode should be turned on.
         */
        setClimbMode(setToClimbMode) {
            this._climbMode = setToClimbMode;
        }
        /**
         * Sets the target speed for this computer, in knots indicated airspeed.
         * @param ias The target speed to set, in knots indicated airspeed.
         */
        setTargetSpeed(ias) {
            this._targetIas = ias;
        }
        /**
         * Deactivates this computer.
         */
        deactivate() {
            this._isActive = false;
            this._pitchTarget.set(null);
        }
        /**
         * Initializes this director on activation.
         */
        initialize() {
            this._lastTime = 0;
            this.pitchController.reset();
            this.filter.reset();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this._isActive) {
                // negate the output value to conform with sim standard.
                this._pitchTarget.set(-this.getDesiredPitch());
            }
            else {
                this._pitchTarget.set(null);
            }
        }
        /**
         * Gets a desired pitch when airborne to maintain a given speed.
         * @returns The desired pitch angle.
         */
        getDesiredPitch() {
            const time = performance.now() / 1000;
            let dt = time - this._lastTime;
            if (this._lastTime === 0) {
                dt = 0;
            }
            const currentIas = SimVar.GetSimVarValue('AIRSPEED INDICATED:1', exports.SimVarValueType.Knots);
            // remember PLANE PITCH DEGREES returns a negative value for up
            const currentPitch = -SimVar.GetSimVarValue('PLANE PITCH DEGREES', exports.SimVarValueType.Degree);
            //step 1 - we want to find the IAS error from target and set a target acceleration
            const iasError = currentIas - this._targetIas;
            const targetAcceleration = MathUtils.clamp(iasError / 5, -2, 2) * -1;
            //step 2 - we want to find the current acceleration, feed that to the pid to manage to the target acceleration
            const acceleration = UnitType.FOOT.convertTo(SimVar.GetSimVarValue('ACCELERATION BODY Z', 'feet per second squared'), UnitType.NMILE) * 3600;
            const accelerationError = acceleration - targetAcceleration;
            const pitchCorrection = this.pitchController.getOutput(dt, accelerationError);
            const aoa = SimVar.GetSimVarValue('INCIDENCE ALPHA', exports.SimVarValueType.Degree);
            this._lastTime = time;
            let targetPitch = isNaN(pitchCorrection) ? currentPitch - aoa : (currentPitch - aoa) + pitchCorrection;
            targetPitch = this.filter.next(targetPitch, dt);
            if (this._climbMode) {
                return Math.max(targetPitch + aoa, aoa);
            }
            else {
                return Math.min(targetPitch + aoa, aoa);
            }
        }
    }

    /**
     * A Flight Level Change autopilot director.
     */
    class APFLCDirector {
        /**
         * Creates a new instance of APFLCDirector.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director. Option values default to the following if not defined:
         * * `maxPitchUpAngle`: `15`
         * * `maxPitchDownAngle`: `15`
         * * `setSpeedOnActivation`: A function which sets the selected IAS or mach target to the airplane's current IAS or
         * mach, depending on whether IAS or mach is currently being targeted.
         */
        constructor(apValues, options) {
            var _a, _b, _c, _d, _e;
            this.apValues = apValues;
            this.setSpeedCommand = {
                ias: undefined,
                mach: undefined,
                isSelectedSpeedInMach: undefined
            };
            const maxPitchUpAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxPitchUpAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxPitchUpAngleOpt) {
                case 'number':
                    this.maxPitchUpAngleFunc = () => maxPitchUpAngleOpt;
                    break;
                case 'function':
                    this.maxPitchUpAngleFunc = maxPitchUpAngleOpt;
                    break;
                default:
                    this.maxPitchUpAngleFunc = () => 15;
            }
            const maxPitchDownAngleOpt = (_b = options === null || options === void 0 ? void 0 : options.maxPitchDownAngle) !== null && _b !== void 0 ? _b : undefined;
            switch (typeof maxPitchDownAngleOpt) {
                case 'number':
                    this.maxPitchDownAngleFunc = () => maxPitchDownAngleOpt;
                    break;
                case 'function':
                    this.maxPitchDownAngleFunc = maxPitchDownAngleOpt;
                    break;
                default:
                    this.maxPitchDownAngleFunc = () => 15;
            }
            this.setSpeedOnActivationFunc = (_c = options === null || options === void 0 ? void 0 : options.setSpeedOnActivation) !== null && _c !== void 0 ? _c : APFLCDirector.defaultSetSpeedOnActivation;
            this.useIndicatedMach = (_d = options === null || options === void 0 ? void 0 : options.useIndicatedMach) !== null && _d !== void 0 ? _d : false;
            this.state = exports.DirectorState.Inactive;
            this.flcComputer = (_e = options === null || options === void 0 ? void 0 : options.flcComputer) !== null && _e !== void 0 ? _e : new GenericFlcComputer({ kP: 2, kI: 0, kD: 0, maxOut: 90, minOut: -90 });
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            this.onActivate && this.onActivate();
            // Handle setting selected speed on activation.
            this.setSpeedCommand.ias = undefined;
            this.setSpeedCommand.mach = undefined;
            this.setSpeedCommand.isSelectedSpeedInMach = undefined;
            const ias = SimVar.GetSimVarValue('AIRSPEED INDICATED:1', exports.SimVarValueType.Knots);
            let mach;
            if (this.useIndicatedMach) {
                mach = AeroMath.casToMach(UnitType.KNOT.convertTo(ias, UnitType.MPS), SimVar.GetSimVarValue('AMBIENT PRESSURE', exports.SimVarValueType.HPA));
            }
            else {
                mach = SimVar.GetSimVarValue('AIRSPEED MACH', exports.SimVarValueType.Mach);
            }
            this.setSpeedOnActivationFunc(ias, mach, this.apValues.isSelectedSpeedInMach.get(), this.setSpeedCommand);
            if (this.setSpeedCommand.ias !== undefined) {
                SimVar.SetSimVarValue('K:AP_SPD_VAR_SET', exports.SimVarValueType.Number, this.setSpeedCommand.ias);
            }
            if (this.setSpeedCommand.mach !== undefined) {
                SimVar.SetSimVarValue('K:AP_MACH_VAR_SET', exports.SimVarValueType.Number, Math.round(this.setSpeedCommand.mach * 100));
            }
            if (this.setSpeedCommand.isSelectedSpeedInMach !== undefined) {
                SimVar.SetSimVarValue(this.setSpeedCommand.isSelectedSpeedInMach ? 'K:AP_MANAGED_SPEED_IN_MACH_ON' : 'K:AP_MANAGED_SPEED_IN_MACH_OFF', exports.SimVarValueType.Number, 0);
            }
            // Activate sim FLC hold and initialize FLC computer climb mode state
            SimVar.SetSimVarValue('AUTOPILOT FLIGHT LEVEL CHANGE', 'Bool', true);
            const currentAltitude = SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet);
            this.flcComputer.activate(this.apValues.selectedAltitude.get() > currentAltitude);
        }
        /**
         * Arms this director.
         * This director can be armed, but it will never automatically activate and remain in the armed state.
         * Therefore we do not resume subs until activation.
         */
        arm() {
            this.state = exports.DirectorState.Armed;
            this.onArm && this.onArm();
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT FLIGHT LEVEL CHANGE', 'Bool', false);
            this.flcComputer.deactivate();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state !== exports.DirectorState.Active) {
                return;
            }
            const currentAltitude = SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet);
            this.flcComputer.setClimbMode(this.apValues.selectedAltitude.get() > currentAltitude);
            if (this.apValues.isSelectedSpeedInMach.get()) {
                const mach = this.apValues.selectedMach.get();
                let ias;
                if (this.useIndicatedMach) {
                    ias = UnitType.KNOT.convertFrom(AeroMath.machToCas(mach, SimVar.GetSimVarValue('AMBIENT PRESSURE', exports.SimVarValueType.HPA)), UnitType.MPS);
                }
                else {
                    ias = Simplane.getMachToKias(mach);
                    if (!isFinite(ias)) {
                        // Sometimes getMachToKias returns a NaN value. If so, fall back to doing the conversion ourselves.
                        ias = UnitType.KNOT.convertFrom(AeroMath.machToCas(mach, SimVar.GetSimVarValue('AMBIENT PRESSURE', exports.SimVarValueType.HPA)), UnitType.MPS);
                    }
                }
                this.flcComputer.setTargetSpeed(ias);
            }
            else {
                this.flcComputer.setTargetSpeed(this.apValues.selectedIas.get());
            }
            this.flcComputer.update();
            const pitchTarget = this.flcComputer.pitchTarget.get();
            // The flcComputer takes care of the aoa adjustment since it needs aoa anyhow,
            // and there is no vertical wind correction for an FLC mode.
            pitchTarget !== null && this.drivePitch && this.drivePitch(MathUtils.clamp(pitchTarget, -this.maxPitchUpAngleFunc(), this.maxPitchDownAngleFunc()), false, false);
        }
        /**
         * Executes default logic for setting selected speed targets when the FLC director is activated. If the current
         * selected speed target is in IAS, then the selected IAS target will be set to the airplane's current indicated
         * airspeed. If the current selected speed target is in mach, then the selected mach target will be set to the
         * airplane's current mach number.
         * @param currentIas The airplane's current indicated airspeed, in knots.
         * @param currentMach The airplane's current mach number.
         * @param isSelectedSpeedInMach Whether the current selected speed target is in mach.
         * @param command The command to set selected speed targets.
         */
        static defaultSetSpeedOnActivation(currentIas, currentMach, isSelectedSpeedInMach, command) {
            if (isSelectedSpeedInMach) {
                command.mach = currentMach;
            }
            else {
                command.ias = currentIas;
            }
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A flight path angle autopilot director.
     */
    class APFPADirector {
        /**
         * Creates an instance of the FPA Director.
         * @param apValues are the ap selected values for the autopilot.
         * @param options Options to configure the new director. Option values default to the following if not defined:
         * * `maxFpa`: `undefined`
         */
        constructor(apValues, options) {
            var _a;
            this.apValues = apValues;
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxFpa) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxFpaFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxFpaFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxFpaFunc = () => Infinity;
            }
            this.selectedFpa = this.apValues.selectedFlightPathAngle.map(fpa => {
                const maxFpa = this.maxFpaFunc();
                return -MathUtils.clamp(fpa, -maxFpa, maxFpa);
            });
            this.state = exports.DirectorState.Inactive;
            this.pauseSubs();
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.selectedFpa.resume();
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.selectedFpa.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            const fpa = this.getCurrentFpa();
            SimVar.SetSimVarValue('L:WT_AP_FPA_Target:1', 'degree', fpa);
            SimVar.SetSimVarValue('AUTOPILOT VERTICAL HOLD', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            this.resumeSubs();
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT VERTICAL HOLD', 'Bool', false);
            this.pauseSubs();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.drivePitch && this.drivePitch(this.selectedFpa.get(), true, true);
            }
        }
        /**
         * Gets the current aircraft FPA.
         * @returns The current aircraft FPA, in degrees.
         */
        getCurrentFpa() {
            const aoa = SimVar.GetSimVarValue('INCIDENCE ALPHA', exports.SimVarValueType.Degree);
            const pitch = -SimVar.GetSimVarValue('PLANE PITCH DEGREES', exports.SimVarValueType.Degree);
            return pitch - aoa;
        }
    }

    exports.APVerticalModes = void 0;
    (function (APVerticalModes) {
        APVerticalModes[APVerticalModes["NONE"] = 0] = "NONE";
        APVerticalModes[APVerticalModes["PITCH"] = 1] = "PITCH";
        APVerticalModes[APVerticalModes["VS"] = 2] = "VS";
        APVerticalModes[APVerticalModes["FLC"] = 3] = "FLC";
        APVerticalModes[APVerticalModes["ALT"] = 4] = "ALT";
        APVerticalModes[APVerticalModes["PATH"] = 5] = "PATH";
        APVerticalModes[APVerticalModes["GP"] = 6] = "GP";
        APVerticalModes[APVerticalModes["GS"] = 7] = "GS";
        APVerticalModes[APVerticalModes["CAP"] = 8] = "CAP";
        APVerticalModes[APVerticalModes["TO"] = 9] = "TO";
        APVerticalModes[APVerticalModes["GA"] = 10] = "GA";
        APVerticalModes[APVerticalModes["FPA"] = 11] = "FPA";
        APVerticalModes[APVerticalModes["FLARE"] = 12] = "FLARE";
        APVerticalModes[APVerticalModes["LEVEL"] = 13] = "LEVEL";
    })(exports.APVerticalModes || (exports.APVerticalModes = {}));
    exports.APLateralModes = void 0;
    (function (APLateralModes) {
        APLateralModes[APLateralModes["NONE"] = 0] = "NONE";
        APLateralModes[APLateralModes["ROLL"] = 1] = "ROLL";
        APLateralModes[APLateralModes["LEVEL"] = 2] = "LEVEL";
        APLateralModes[APLateralModes["GPSS"] = 3] = "GPSS";
        APLateralModes[APLateralModes["HEADING"] = 4] = "HEADING";
        APLateralModes[APLateralModes["VOR"] = 5] = "VOR";
        APLateralModes[APLateralModes["LOC"] = 6] = "LOC";
        APLateralModes[APLateralModes["BC"] = 7] = "BC";
        APLateralModes[APLateralModes["ROLLOUT"] = 8] = "ROLLOUT";
        APLateralModes[APLateralModes["NAV"] = 9] = "NAV";
        APLateralModes[APLateralModes["TO"] = 10] = "TO";
        APLateralModes[APLateralModes["GA"] = 11] = "GA";
        APLateralModes[APLateralModes["HEADING_HOLD"] = 12] = "HEADING_HOLD";
        APLateralModes[APLateralModes["TRACK"] = 13] = "TRACK";
        APLateralModes[APLateralModes["TRACK_HOLD"] = 14] = "TRACK_HOLD";
        APLateralModes[APLateralModes["FMS_LOC"] = 15] = "FMS_LOC";
        APLateralModes[APLateralModes["TO_LOC"] = 16] = "TO_LOC";
    })(exports.APLateralModes || (exports.APLateralModes = {}));
    exports.APAltitudeModes = void 0;
    (function (APAltitudeModes) {
        APAltitudeModes[APAltitudeModes["NONE"] = 0] = "NONE";
        APAltitudeModes[APAltitudeModes["ALTS"] = 1] = "ALTS";
        APAltitudeModes[APAltitudeModes["ALTV"] = 2] = "ALTV";
    })(exports.APAltitudeModes || (exports.APAltitudeModes = {}));

    /**
     * The current vertical navigation state.
     */
    exports.VNavState = void 0;
    (function (VNavState) {
        /** VNAV Disabled. */
        VNavState[VNavState["Disabled"] = 0] = "Disabled";
        /** VNAV Enabled and Inactive. */
        VNavState[VNavState["Enabled_Inactive"] = 1] = "Enabled_Inactive";
        /** VNAV Enabled and Active. */
        VNavState[VNavState["Enabled_Active"] = 2] = "Enabled_Active";
    })(exports.VNavState || (exports.VNavState = {}));
    /**
     * The current VNAV path mode.
     */
    exports.VNavPathMode = void 0;
    (function (VNavPathMode) {
        /** VNAV path is not active. */
        VNavPathMode[VNavPathMode["None"] = 0] = "None";
        /** VNAV path is armed for capture. */
        VNavPathMode[VNavPathMode["PathArmed"] = 1] = "PathArmed";
        /** VNAV path is actively navigating. */
        VNavPathMode[VNavPathMode["PathActive"] = 2] = "PathActive";
        /** The current VNAV path is not valid. */
        VNavPathMode[VNavPathMode["PathInvalid"] = 3] = "PathInvalid";
    })(exports.VNavPathMode || (exports.VNavPathMode = {}));
    /**
     * The current Approach Guidance Mode.
     */
    exports.ApproachGuidanceMode = void 0;
    (function (ApproachGuidanceMode) {
        /** VNAV is not currently following approach guidance. */
        ApproachGuidanceMode[ApproachGuidanceMode["None"] = 0] = "None";
        /** VNAV has armed ILS glideslope guidance for capture. */
        ApproachGuidanceMode[ApproachGuidanceMode["GSArmed"] = 1] = "GSArmed";
        /** VNAV is actively following ILS glideslope guidance. */
        ApproachGuidanceMode[ApproachGuidanceMode["GSActive"] = 2] = "GSActive";
        /** VNAV RNAV glidepath guidance is armed for capture. */
        ApproachGuidanceMode[ApproachGuidanceMode["GPArmed"] = 3] = "GPArmed";
        /** VNAV is actively follow RNAV glidepath guidance. */
        ApproachGuidanceMode[ApproachGuidanceMode["GPActive"] = 4] = "GPActive";
    })(exports.ApproachGuidanceMode || (exports.ApproachGuidanceMode = {}));
    /**
     * The current VNAV altitude capture type.
     */
    exports.VNavAltCaptureType = void 0;
    (function (VNavAltCaptureType) {
        /** Altitude capture is not armed. */
        VNavAltCaptureType[VNavAltCaptureType["None"] = 0] = "None";
        /** Altitude will capture the selected altitude. */
        VNavAltCaptureType[VNavAltCaptureType["Selected"] = 1] = "Selected";
        /** Altitude will capture the VANV target altitude. */
        VNavAltCaptureType[VNavAltCaptureType["VNAV"] = 2] = "VNAV";
    })(exports.VNavAltCaptureType || (exports.VNavAltCaptureType = {}));
    /**
     * The current state of VNAV availability from the director.
     */
    exports.VNavAvailability = void 0;
    (function (VNavAvailability) {
        VNavAvailability["Available"] = "Available";
        VNavAvailability["InvalidLegs"] = "InvalidLegs";
    })(exports.VNavAvailability || (exports.VNavAvailability = {}));

    /**
     * SimVar names for VNAV data.
     */
    exports.VNavVars = void 0;
    (function (VNavVars) {
        /** The vertical deviation in feet. */
        VNavVars["VerticalDeviation"] = "L:WTAP_VNav_Vertical_Deviation";
        /** The VNAV target altitude in feet. */
        VNavVars["TargetAltitude"] = "L:WTAP_VNav_Target_Altitude";
        /** The VNAV path mode. */
        VNavVars["PathMode"] = "L:WTAP_VNav_Path_Mode";
        /** The VNAV State. */
        VNavVars["VNAVState"] = "L:WTAP_VNav_State";
        /** Whether a VNAV Path Exists for the current leg. */
        VNavVars["PathAvailable"] = "L:WTAP_VNav_Path_Available";
        /** The VNAV current altitude capture type. */
        VNavVars["CaptureType"] = "L:WTAP_VNav_Alt_Capture_Type";
        /** The distance to the next TOD in meters, or -1 if one does not exist. */
        VNavVars["TODDistance"] = "L:WTAP_VNav_Distance_To_TOD";
        /** The distance to the next BOD in meters, or -1 if one does not exist. */
        VNavVars["BODDistance"] = "L:WTAP_VNav_Distance_To_BOD";
        /** The index of the leg for the next TOD. */
        VNavVars["TODLegIndex"] = "L:WTAP_VNav_TOD_Leg_Index";
        /** The distance from the end of the TOD leg that the TOD is, in meters. */
        VNavVars["TODDistanceInLeg"] = "L:WTAP_VNav_TOD_Distance_In_Leg";
        /** The index of the leg for the next BOD. */
        VNavVars["BODLegIndex"] = "L:WTAP_VNav_BOD_Leg_Index";
        /** The distance to the next TOC in meters, or -1 if one does not exist. */
        VNavVars["TOCDistance"] = "L:WTAP_VNav_Distance_To_TOC";
        /** The distance to the next BOC in meters, or -1 if one does not exist. */
        VNavVars["BOCDistance"] = "L:WTAP_VNav_Distance_To_BOC";
        /** The index of the leg for the next TOC. */
        VNavVars["TOCLegIndex"] = "L:WTAP_VNav_TOC_Leg_Index";
        /** The distance from the end of the TOC leg that the TOC is, in meters. */
        VNavVars["TOCDistanceInLeg"] = "L:WTAP_VNav_TOC_Distance_In_Leg";
        /** The index of the leg for the next BOC. */
        VNavVars["BOCLegIndex"] = "L:WTAP_VNav_BOC_Leg_Index";
        /** The index of the leg for the next constraint. */
        VNavVars["CurrentConstraintLegIndex"] = "L:WTAP_VNav_Constraint_Leg_Index";
        /** The current constraint altitude, in feet. */
        VNavVars["CurrentConstraintAltitude"] = "L:WTAP_VNav_Constraint_Altitude";
        /** The next constraint altitude, in feet. */
        VNavVars["NextConstraintAltitude"] = "L:WTAP_VNav_Next_Constraint_Altitude";
        /** The current required flight path angle, in degrees. */
        VNavVars["FPA"] = "L:WTAP_VNav_FPA";
        /** The required VS to the current constraint, in FPM. */
        VNavVars["RequiredVS"] = "L:WTAP_VNAV_Required_VS";
        /** The VNAV approach guidance mode. */
        VNavVars["GPApproachMode"] = "L:WTAP_GP_Approach_Mode";
        /** The current LPV vertical deviation in feet. */
        VNavVars["GPVerticalDeviation"] = "L:WTAP_GP_Vertical_Deviation";
        /** The current remaining LPV distance in meters. */
        VNavVars["GPDistance"] = "L:WTAP_GP_Distance";
        /** The current LPV FPA, in degrees. */
        VNavVars["GPFpa"] = "L:WTAP_GP_FPA";
        /** The required VS to the current constraint, in FPM. */
        VNavVars["GPRequiredVS"] = "L:WTAP_GP_Required_VS";
        /** The approach glidepath service level. */
        VNavVars["GPServiceLevel"] = "L:WTAP_GP_Service_Level";
    })(exports.VNavVars || (exports.VNavVars = {}));
    /**
     * A publisher for VNAV events derived from SimVars.
     */
    class VNavSimVarPublisher extends SimVarPublisher {
        /**
         * Creates a new instance of VNavSimVarPublisher.
         * @param bus The event bus to which to publish.
         */
        constructor(bus) {
            const defs = [
                ['gp_approach_mode', { name: exports.VNavVars.GPApproachMode, type: exports.SimVarValueType.Number }],
                ...ArrayUtils.flatMap([
                    ['vnav_vertical_deviation', { name: exports.VNavVars.VerticalDeviation, type: exports.SimVarValueType.Feet }],
                    ['vnav_target_altitude', { name: exports.VNavVars.TargetAltitude, type: exports.SimVarValueType.Feet }],
                    ['vnav_path_mode', { name: exports.VNavVars.PathMode, type: exports.SimVarValueType.Number }],
                    ['vnav_path_available', { name: exports.VNavVars.PathAvailable, type: exports.SimVarValueType.Bool }],
                    ['vnav_state', { name: exports.VNavVars.VNAVState, type: exports.SimVarValueType.Number }],
                    ['vnav_altitude_capture_type', { name: exports.VNavVars.CaptureType, type: exports.SimVarValueType.Number }],
                    ['vnav_tod_distance', { name: exports.VNavVars.TODDistance, type: exports.SimVarValueType.Meters }],
                    ['vnav_tod_leg_distance', { name: exports.VNavVars.TODDistanceInLeg, type: exports.SimVarValueType.Meters }],
                    ['vnav_bod_distance', { name: exports.VNavVars.BODDistance, type: exports.SimVarValueType.Meters }],
                    ['vnav_tod_global_leg_index', { name: exports.VNavVars.TODLegIndex, type: exports.SimVarValueType.Number }],
                    ['vnav_bod_global_leg_index', { name: exports.VNavVars.BODLegIndex, type: exports.SimVarValueType.Number }],
                    ['vnav_toc_distance', { name: exports.VNavVars.TOCDistance, type: exports.SimVarValueType.Meters }],
                    ['vnav_toc_leg_distance', { name: exports.VNavVars.TOCDistanceInLeg, type: exports.SimVarValueType.Meters }],
                    ['vnav_boc_distance', { name: exports.VNavVars.BOCDistance, type: exports.SimVarValueType.Meters }],
                    ['vnav_toc_global_leg_index', { name: exports.VNavVars.TOCLegIndex, type: exports.SimVarValueType.Number }],
                    ['vnav_boc_global_leg_index', { name: exports.VNavVars.BOCLegIndex, type: exports.SimVarValueType.Number }],
                    ['vnav_constraint_global_leg_index', { name: exports.VNavVars.CurrentConstraintLegIndex, type: exports.SimVarValueType.Number }],
                    ['vnav_constraint_altitude', { name: exports.VNavVars.CurrentConstraintAltitude, type: exports.SimVarValueType.Feet }],
                    ['vnav_next_constraint_altitude', { name: exports.VNavVars.NextConstraintAltitude, type: exports.SimVarValueType.Feet }],
                    ['vnav_fpa', { name: exports.VNavVars.FPA, type: exports.SimVarValueType.Degree }],
                    ['vnav_required_vs', { name: exports.VNavVars.RequiredVS, type: exports.SimVarValueType.FPM }],
                    ['gp_vertical_deviation', { name: exports.VNavVars.GPVerticalDeviation, type: exports.SimVarValueType.Feet }],
                    ['gp_distance', { name: exports.VNavVars.GPDistance, type: exports.SimVarValueType.Feet }],
                    ['gp_fpa', { name: exports.VNavVars.GPFpa, type: exports.SimVarValueType.Degree }],
                    ['gp_required_vs', { name: exports.VNavVars.GPRequiredVS, type: exports.SimVarValueType.FPM }],
                    ['gp_service_level', { name: exports.VNavVars.GPServiceLevel, type: exports.SimVarValueType.Number }],
                ], pair => {
                    const [topic, entry] = pair;
                    const indexedEntry = {
                        name: `${entry.name}:#index#`,
                        type: entry.type,
                        indexed: true,
                        defaultIndex: null
                    };
                    return [
                        pair,
                        [topic, indexedEntry]
                    ];
                })
            ];
            super(defs, bus);
        }
    }

    /**
     * An RNAV LPV glidepath autopilot director.
     */
    class APGPDirector {
        /**
         * Creates a new instance of APGPDirector.
         * @param bus The event bus.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options with which to configure the director.
         */
        constructor(bus, apValues, options) {
            var _a, _b, _c, _d;
            this.apValues = apValues;
            this.deviationSimVar = exports.VNavVars.GPVerticalDeviation;
            this.fpaSimVar = exports.VNavVars.GPFpa;
            if (options === null || options === void 0 ? void 0 : options.guidance) {
                this.guidance = options.guidance;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.isGuidanceValidFunc = () => this.guidance.get().isValid;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.getFpaFunc = () => this.guidance.get().fpa;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.getDeviationFunc = () => this.guidance.get().deviation;
            }
            else {
                this.vnavIndex = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.vnavIndex) !== null && _a !== void 0 ? _a : 0, true);
                this.vnavIndex.sub(index => {
                    if (VNavUtils.isValidVNavIndex(index)) {
                        const suffix = index === 0 ? '' : `:${index}`;
                        this.deviationSimVar = `${exports.VNavVars.GPVerticalDeviation}${suffix}`;
                        this.fpaSimVar = `${exports.VNavVars.GPFpa}${suffix}`;
                    }
                });
                this.isGuidanceValidFunc = () => true;
                this.getFpaFunc = SimVar.GetSimVarValue.bind(undefined, this.fpaSimVar, exports.SimVarValueType.Degree);
                this.getDeviationFunc = SimVar.GetSimVarValue.bind(undefined, this.deviationSimVar, exports.SimVarValueType.Feet);
            }
            this.canArmFunc = (_b = options === null || options === void 0 ? void 0 : options.canArm) !== null && _b !== void 0 ? _b : APGPDirector.defaultCanArm;
            this.canCaptureFunc = (_c = options === null || options === void 0 ? void 0 : options.canCapture) !== null && _c !== void 0 ? _c : APGPDirector.defaultCanCapture.bind(undefined, this.apValues);
            this.canTrackFunc = (_d = options === null || options === void 0 ? void 0 : options.canTrack) !== null && _d !== void 0 ? _d : APGPDirector.defaultCanTrack.bind(undefined, this.apValues);
            this.state = exports.DirectorState.Inactive;
            apValues.approachHasGP.sub(hasGp => {
                if (this.state !== exports.DirectorState.Inactive && !hasGp) {
                    this.deactivate();
                }
            });
        }
        /** @inheritDoc */
        activate() {
            this.state = exports.DirectorState.Active;
            SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.GPActive);
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', false);
        }
        /** @inheritDoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                if (this.canArmFunc(this.isGuidanceValidFunc(), this.getFpaFunc(), this.getDeviationFunc())) {
                    this.state = exports.DirectorState.Armed;
                    SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.GPArmed);
                    if (this.onArm !== undefined) {
                        this.onArm();
                    }
                    SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', true);
                    SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', false);
                    SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
                }
            }
        }
        /** @inheritDoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.None);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', false);
        }
        /** @inheritDoc */
        update() {
            const isGuidanceValid = this.isGuidanceValidFunc();
            let deviation;
            let fpa;
            if (this.state === exports.DirectorState.Armed) {
                fpa = this.getFpaFunc();
                deviation = this.getDeviationFunc();
                if (!this.canArmFunc(isGuidanceValid, fpa, deviation)) {
                    this.deactivate();
                }
                else if (isGuidanceValid && this.canCaptureFunc(fpa, deviation)) {
                    this.activate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                fpa !== null && fpa !== void 0 ? fpa : (fpa = this.getFpaFunc());
                deviation !== null && deviation !== void 0 ? deviation : (deviation = this.getDeviationFunc());
                if (!isGuidanceValid || !this.canTrackFunc(fpa, deviation)) {
                    this.deactivate();
                    return;
                }
                const groundSpeed = SimVar.GetSimVarValue('GROUND VELOCITY', exports.SimVarValueType.Knots);
                const fpaPercentage = Math.max(deviation / (VNavUtils.getPathErrorDistance(groundSpeed) * -1), -1) + 1;
                this.drivePitch && this.drivePitch(fpa * fpaPercentage, true, true);
            }
        }
        /**
         * Checks whether the director can be armed using default logic.
         * @returns Whether the director can be armed.
         */
        static defaultCanArm() {
            return true;
        }
        /**
         * Checks whether the director can capture a glidepath from an armed state using default logic.
         * @param apValues Autopilot values.
         * @param fpa The flight path angle of the glidepath, in degrees. Positive angles indicate a downward-sloping path.
         * @param deviation The deviation of the glidepath from the airplane, in feet. Positive values indicate the path lies
         * above the airplane.
         * @returns Whether the director can capture the glidepath.
         */
        static defaultCanCapture(apValues, fpa, deviation) {
            return apValues.lateralActive.get() === exports.APLateralModes.GPSS
                && fpa > 0
                && deviation <= 100
                && deviation >= -15;
        }
        /**
         * Checks whether the director can continue tracking a glidepath using default logic.
         * @param apValues Autopilot values.
         * @returns Whether the director can continuing tracking the glidepath.
         */
        static defaultCanTrack(apValues) {
            return apValues.lateralActive.get() === exports.APLateralModes.GPSS;
        }
    }

    /**
     * A utility class for creating easing functions. All generated easing functions are based on their CSS counterparts.
     */
    class Easing {
        /**
         * Creates a linear easing function.
         * @returns A linear easing function.
         */
        static linear() {
            return (x) => MathUtils.clamp(x, 0, 1);
        }
        /**
         * Creates a quadratic easing function.
         * @param end The end to which to apply easing.
         * @returns A quadratic easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static quad(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x * x;
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - (1 - x) * (1 - x);
                case 'both':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x < 0.5 ? 2 * x * x : 1 - 2 * (1 - x) * (1 - x);
                default:
                    throw new Error(`Easing.quad(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a cubic easing function.
         * @param end The end to which to apply easing.
         * @returns A cubic easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static cubic(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x * x * x;
                case 'out':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - compl * compl * compl;
                        }
                    };
                case 'both':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else if (x < 0.5) {
                            return 4 * x * x * x;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - 4 * compl * compl * compl;
                        }
                    };
                default:
                    throw new Error(`Easing.cubic(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a quartic easing function.
         * @param end The end to which to apply easing.
         * @returns A quartic easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static quart(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x * x * x * x;
                case 'out':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - compl * compl * compl * compl;
                        }
                    };
                case 'both':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else if (x < 0.5) {
                            return 8 * x * x * x * x;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - 8 * compl * compl * compl * compl;
                        }
                    };
                default:
                    throw new Error(`Easing.quart(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a quintic easing function.
         * @param end The end to which to apply easing.
         * @returns A quintic easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static quint(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x * x * x * x * x;
                case 'out':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - compl * compl * compl * compl * compl;
                        }
                    };
                case 'both':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else if (x < 0.5) {
                            return 16 * x * x * x * x * x;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - 16 * compl * compl * compl * compl * compl;
                        }
                    };
                default:
                    throw new Error(`Easing.quint(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a polynomial easing function.
         * @param order The order of the polynomial.
         * @param end The end to which to apply easing.
         * @returns A polynomial easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static polynomial(order, end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : Math.pow(x, order);
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - Math.pow(1 - x, order);
                case 'both':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x < 0.5 ? 0.5 * Math.pow(2 * x, order) : 1 - 0.5 * Math.pow(2 * (1 - x), order);
                default:
                    throw new Error(`Easing.polynomial(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a sinusoid easing function.
         * @param end The end to which to apply easing.
         * @returns A sinusoid easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static sin(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - Math.cos(x * MathUtils.HALF_PI);
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : Math.sin(x * MathUtils.HALF_PI);
                case 'both':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : (1 - Math.cos(x * Math.PI)) * 0.5;
                default:
                    throw new Error(`Easing.sin(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a circular easing function.
         * @param end The end to which to apply easing.
         * @returns A circular easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static circ(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - Math.sqrt(1 - x * x);
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : Math.sqrt(1 - (x - 1) * (x - 1));
                case 'both':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x < 0.5 ? (1 - Math.sqrt(1 - 4 * x * x)) * 0.5 : (Math.sqrt(1 - 4 * (1 - x) * (1 - x)) + 1) * 0.5;
                default:
                    throw new Error(`Easing.circ(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates an exponential easing function.
         * @param end The end to which to apply easing.
         * @returns An exponential easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static exp(end) {
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : Math.pow(2, 10 * (x - 1));
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - Math.pow(2, -10 * x);
                case 'both':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : x < 0.5 ? 0.5 * Math.pow(2, 20 * (x - 0.5)) : 1 - 0.5 * Math.pow(2, 20 * (0.5 - x));
                default:
                    throw new Error(`Easing.exp(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a back easing function.
         * @param end The end to which to apply easing.
         * @returns A back easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static back(end) {
            const c1 = 2.70158;
            const c2 = 1.70158;
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : c1 * x * x * x - c2 * x * x;
                case 'out':
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - compl * compl * compl + c2 * compl * compl;
                        }
                    };
                case 'both': {
                    const c3 = c2 * 1.525;
                    const c4 = c3 + 1;
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else if (x < 0.5) {
                            return 2 * (x * x * (2 * c4 * x - c3));
                        }
                        else {
                            const compl = 1 - x;
                            return 1 - 2 * compl * compl * (2 * c4 * compl - c3);
                        }
                    };
                }
                default:
                    throw new Error(`Easing.back(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates an elastic easing function.
         * @param end The end to which to apply easing.
         * @returns An elastic easing function.
         * @throws Error if `end` is not one of `'in' | 'out' | 'both'`.
         */
        static elastic(end) {
            const c1 = MathUtils.TWO_PI / 3;
            switch (end) {
                case 'in':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : c1 * Math.pow(2, 10 * (x - 1)) * Math.sin(10.75 - 10 * x);
                case 'out':
                    return (x) => x <= 0 ? 0 : x >= 1 ? 1 : 1 - c1 * Math.pow(2, -10 * x) * Math.sin(0.75 - 10 * x);
                case 'both': {
                    const c2 = c1 * c1;
                    return (x) => {
                        if (x <= 0) {
                            return 0;
                        }
                        else if (x >= 1) {
                            return 1;
                        }
                        else if (x < 0.5) {
                            return 0.5 * Math.pow(2, 20 * (x - 0.5)) * Math.sin(c2 * (11.125 - 20 * x));
                        }
                        else {
                            return 1 - 0.5 * Math.pow(2, 20 * (0.5 - x)) * Math.sin(c2 * (11.125 - 20 * x));
                        }
                    };
                }
                default:
                    throw new Error(`Easing.elastic(): unrecognized end option '${end}'. Expected 'in' | 'out' | 'both'.`);
            }
        }
        /**
         * Creates a cubic bezier easing function. The function follows a cubic bezier curve with the endpoints fixed at
         * `(0, 0)` and `(1, 1)`.
         *
         * The function can optionally use a precomputed lookup table. Using a lookup table may increase performance and/or
         * precision of the created function at the cost of some precomputation time and memory needed to store the lookup
         * table.
         * @param c1x The x-coordinate of the first control point. Will be clamped to the range `[0, 1]`.
         * @param c1y The y-coordinate of the first control point.
         * @param c2x The x-coordinate of the second control point. Will be clamped to the range `[0, 1]`.
         * @param c2y The y-coordinate of the second control point.
         * @param precompute Whether to precompute a lookup table. Defaults to `false`.
         * @param minXResolution The minimum resolution of the function along the x-axis. Defaults to `0.1` if `precompute`
         * is `true`, or `1e-4` if `precompute` is `false`.
         * @param epsilon The threshold of acceptable linear interpolation error used during precomputation. Ignored if
         * `precompute` is false. Defaults to `1e-4`.
         * @param maxDepth The maximum allowed recursive depth of precomputation. The number of additional lookup table
         * breakpoints generated is bounded from above by `2 ^ (maxDepth) - 1`. Ignored if `precompute` is false. Defaults
         * to `10`.
         * @returns A cubic bezier easing function.
         */
        static bezier(c1x, c1y, c2x, c2y, precompute = false, minXResolution, epsilon = 1e-3, maxDepth = 10) {
            c1x = MathUtils.clamp(c1x, 0, 1);
            c2x = MathUtils.clamp(c2x, 0, 1);
            if (precompute) {
                const minXRes = minXResolution !== null && minXResolution !== void 0 ? minXResolution : 0.1;
                const lookup = new LerpLookupTable(1);
                lookup.insertBreakpoint([0, 0]);
                lookup.insertBreakpoint([1, 1]);
                if (maxDepth > 0) {
                    Easing.precomputeBezier(c1x, c1y, c2x, c2y, lookup, minXRes, epsilon, maxDepth, 0, 0, 0, 1, 1, 1, 1);
                }
                return (x) => {
                    return x <= 0 ? 0 : x >= 1 ? 1 : lookup.get(x);
                };
            }
            else {
                const minXRes = Math.max(minXResolution !== null && minXResolution !== void 0 ? minXResolution : 1e-4, 1e-6);
                return (x) => {
                    if (x <= 0) {
                        return 0;
                    }
                    else if (x >= 1) {
                        return 1;
                    }
                    // Find t for x.
                    let t0 = 0, t1 = 1;
                    let tquery = (t0 + t1) / 2;
                    let xquery = Easing.easingBezierFunc(tquery, c1x, c2x);
                    while (Math.abs(x - xquery) > minXRes) {
                        if (x - xquery < 0) {
                            t1 = tquery;
                        }
                        else {
                            t0 = tquery;
                        }
                        tquery = (t0 + t1) / 2;
                        xquery = Easing.easingBezierFunc(tquery, c1x, c2x);
                    }
                    // Calculate y for t.
                    return Easing.easingBezierFunc(tquery, c1y, c2y);
                };
            }
        }
        /**
         * Precomputes lookup table breakpoints for a cubic bezier easing function.
         * @param c1x The x-coordinate of the first control point.
         * @param c1y The y-coordinate of the first control point.
         * @param c2x The x-coordinate of the second control point.
         * @param c2y The y-coordinate of the second control point.
         * @param lookup A lookup table to which to add breakpoints.
         * @param minXRes The desired minimum resolution along the x-axis.
         * @param epsilon The maximum acceptable linear interpolation error. Recursion will continue if the interpolated y
         * value differs from the exact y value by more than this amount.
         * @param maxDepth The maximum allowed recursion depth.
         * @param t0 The distance value at the current left endpoint.
         * @param x0 The x value at the current left endpoint.
         * @param y0 The y value at the current left endpoint.
         * @param t1 The distance value at the current right endpoint.
         * @param x1 The x value at the current right endpoint.
         * @param y1 The y value at the current right endpoint.
         * @param depth The current recursion depth.
         */
        static precomputeBezier(c1x, c1y, c2x, c2y, lookup, minXRes, epsilon, maxDepth, t0, x0, y0, t1, x1, y1, depth) {
            const tmid = (t0 + t1) / 2;
            const xdelta = x1 - x0;
            const xmid = Easing.easingBezierFunc(tmid, c1x, c2x);
            const ymid = Easing.easingBezierFunc(tmid, c1y, c2y);
            const ylerp = MathUtils.lerp(xmid, x0, x1, y0, y1);
            let shouldContinue = false;
            shouldContinue = xdelta > minXRes || Math.abs(ylerp - ymid) > epsilon;
            if (shouldContinue) {
                lookup.insertBreakpoint([ymid, xmid]);
                if (depth < maxDepth) {
                    Easing.precomputeBezier(c1x, c1y, c2x, c2y, lookup, minXRes, epsilon, maxDepth, t0, x0, y0, tmid, xmid, ymid, depth + 1);
                    Easing.precomputeBezier(c1x, c1y, c2x, c2y, lookup, minXRes, epsilon, maxDepth, tmid, xmid, ymid, t1, x1, y1, depth + 1);
                }
            }
        }
        /**
         * Computes a coordinate value along a bezier curve with P0 fixed at `(0, 0)` and P3 fixed at `(1, 1)`.
         * @param t The distance along the curve normalized to `[0, 1]`.
         * @param c1 The coordinate of the first control point.
         * @param c2 The coordinate of the second control point.
         * @returns The coordinate value along the specified bezier curve at the specified distance.
         */
        static easingBezierFunc(t, c1, c2) {
            return 3 * (1 - t) * (1 - t) * t * c1 + 3 * (1 - t) * t * t * c2 + t * t * t;
        }
        /**
         * Converts an easing function to one which supports arbitrary start and stop endpoints.
         * @param ease An easing function.
         * @returns A new easing function which generates the same shape as the specified function while supporting arbitrary
         * start and stop endpoints.
         */
        static withEndpointParams(ease) {
            return (start, stop, progress) => {
                return start + (stop - start) * ease(progress);
            };
        }
        /**
         * Converts an easing function to one which uses specific start and stop endpoints.
         * @param ease An easing function.
         * @param start The start endpoint.
         * @param stop The stop endpoint.
         * @returns A new easing function which generates the same shape as the specified function while using the specified
         * start and stop endpoints.
         */
        static withEndpoints(ease, start, stop) {
            const delta = stop - start;
            return (progress) => {
                return start + delta * ease(progress);
            };
        }
    }

    /**
     * An animator.
     */
    class Animator {
        constructor() {
            this._value = Subject.create(0);
            this._isAnimating = false;
            this.isAnimationLoopActive = false;
            this.animationEaseFunc = Animator.DEFAULT_EASE_FUNC;
            this.animationStart = 0;
            this.animationStop = 0;
            this.animationStartTime = 0;
            this.animationDuration = 0;
            this.animationLoop = () => {
                if (!this._isAnimating) {
                    this.isAnimationLoopActive = false;
                    return;
                }
                const progress = (Date.now() - this.animationStartTime) / this.animationDuration;
                if (progress < 1) {
                    this._value.set(this.animationEaseFunc(this.animationStart, this.animationStop, progress));
                    requestAnimationFrame(this.animationLoop);
                }
                else {
                    this._isAnimating = false;
                    this.isAnimationLoopActive = false;
                    this._value.set(this.animationStop);
                }
            };
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** This animator's current value. */
        get value() {
            return this._value;
        }
        /**
         * Checks whether this animator has an animation in progress.
         * @returns Whether this animator has an animation in progress.
         */
        isAnimating() {
            return this._isAnimating;
        }
        /**
         * Starts an animation. The animation will proceed from this animator's current value to the target value over the
         * specified duration. If another animation is currently active, it will immediately be stopped and replaced by the
         * new animation.
         * @param target The target value.
         * @param duration The duration of the animation, in milliseconds.
         * @param easeFunc The easing function to apply to the animation. Defaults to a linear easing function.
         */
        start(target, duration, easeFunc) {
            if (duration <= 0) {
                this.set(target);
                return;
            }
            this._isAnimating = true;
            this.animationStart = this._value.get();
            this.animationStop = target;
            this.animationStartTime = Date.now();
            this.animationDuration = duration;
            this.animationEaseFunc = easeFunc !== null && easeFunc !== void 0 ? easeFunc : Animator.DEFAULT_EASE_FUNC;
            if (!this.isAnimationLoopActive) {
                this.isAnimationLoopActive = true;
                requestAnimationFrame(this.animationLoop);
            }
        }
        /**
         * Immediately sets this animator's value. This will stop any animation currently in progress.
         * @param value The rotation to set, in degrees.
         */
        set(value) {
            this._isAnimating = false;
            this._value.set(value);
        }
        /**
         * Stops this animator's current in-progress animation, if any, and optionally sets this animator's value to the
         * animation's target value.
         * @param setAnimationTarget Whether to set this animator's value to the animation target value after stopping the
         * animation. Defaults to `false`.
         */
        stop(setAnimationTarget = false) {
            if (!this._isAnimating) {
                return;
            }
            this._isAnimating = false;
            if (setAnimationTarget) {
                this._value.set(this.animationStop);
            }
        }
    }
    Animator.DEFAULT_EASE_FUNC = Easing.withEndpointParams(Easing.linear());

    /**
     * A controller for automated backlighting levels based upon the angle of the sun in the sky.
     */
    class BacklightLevelController {
        /**
         * Creates an automatic backlight controller.
         * @param bus The event bus.
         * @param paused Whether the controller should be initially paused. Defaults to `false`.
         * @param minIntensity The maximum intensity commanded by the controller. Defaults to 0.
         * @param maxIntensity The minimum intensity commanded by the controller. Defaults to 1.
         */
        constructor(bus, paused = false, minIntensity = BacklightLevelController.DEFAULT_MIN_INTENSITY, maxIntensity = BacklightLevelController.DEFAULT_MAX_INTENSITY) {
            this.simTime = ConsumerValue.create(null, 0);
            this.ppos = new Float64Array(3);
            this.altitude = 0;
            this.needRecalcAuto = true;
            this.lastSimTime = 0;
            this.paused = false;
            this._intensity = Subject.create(0);
            /** The automatic backlight intensity computed by this controller. */
            this.intensity = this._intensity;
            this._autoMinIntensity = minIntensity;
            this._autoMaxIntensity = maxIntensity;
            this.needRecalcAuto = true;
            const sub = bus.getSubscriber();
            this.simTime.setConsumer(sub.on('simTime'));
            this.pposSub = sub.on('gps-position').atFrequency(BacklightLevelController.AUTO_UPDATE_REALTIME_FREQ).handle(this.onPPosChanged.bind(this));
            this.updateSub = sub.on('realTime').atFrequency(BacklightLevelController.AUTO_UPDATE_REALTIME_FREQ).handle(this.onUpdate.bind(this));
            this.setPaused(paused);
        }
        /**
         * Get the max auto intensity value
         * @returns The maximum intensity applied by the auto backlight.
         */
        get autoMaxIntensity() {
            return this._autoMaxIntensity;
        }
        /**
         * Set the max auto intensity value.
         * @param max_intensity The maximum intensity applied by auto backlight.
         */
        set autoMaxIntensity(max_intensity) {
            this._autoMaxIntensity = max_intensity;
            this.needRecalcAuto = true;
        }
        /**
         * Get the min auto intensity value
         * @returns THe minimum intensity applied by the auto backlight.
         */
        get autoMinIntensity() {
            return this._autoMinIntensity;
        }
        /**
         * Set the min auto intensity value.
         * @param min_intensity The minimum intensity applied by the auto backlight.
         */
        set autoMinIntensity(min_intensity) {
            this._autoMinIntensity = min_intensity;
            this.needRecalcAuto = true;
        }
        /**
         * Pause or unpause real-time processing.
         * @param paused Whether to pause or not.
         */
        setPaused(paused) {
            if (paused !== this.paused) {
                this.paused = paused;
                if (paused) {
                    this.updateSub.pause();
                    this.pposSub.pause();
                    this.simTime.pause();
                    this.needRecalcAuto = false;
                }
                else {
                    this.needRecalcAuto = true;
                    this.simTime.resume();
                    this.pposSub.resume(true);
                    this.updateSub.resume(true);
                }
            }
        }
        /**
         * A callback which is called when the user's location changes.
         * @param ppos The new plane position.
         */
        onPPosChanged(ppos) {
            const pposVec = GeoPoint.sphericalToCartesian(ppos.lat, ppos.long, BacklightLevelController.tempVec3);
            // If the new position is within ~3000 m laterally and 60 m vertically of the last position used to calculate the
            // backlight, there's no need to update the calculation.
            if (Vec3Math.dot(pposVec, this.ppos) >= 0.999999875 && Math.abs(ppos.alt - this.altitude) <= 60) {
                return;
            }
            Vec3Math.copy(pposVec, this.ppos);
            this.altitude = ppos.alt;
            this.needRecalcAuto = true;
        }
        /**
         * Updates this controller's commanded backlight intensity if necessary.
         */
        onUpdate() {
            const simTime = this.simTime.get();
            this.needRecalcAuto || (this.needRecalcAuto = Math.abs(simTime - this.lastSimTime) >= BacklightLevelController.AUTO_UPDATE_SIMTIME_THRESHOLD);
            if (this.needRecalcAuto) {
                this.needRecalcAuto = false;
                this.updateAutoBacklightIntensity(simTime);
            }
        }
        /**
         * Updates this controller's commanded backlight intensity according to the auto setting algorithm.
         * @param simTime The current sim time.
         */
        updateAutoBacklightIntensity(simTime) {
            this.lastSimTime = simTime;
            const subSolarPoint = BacklightLevelController.calculateSubSolarPoint(simTime, BacklightLevelController.tempVec3);
            // The sun is far enough from the earth (~1.5e11 meters) compared to one earth radius (~6.4e6 meters) that for the
            // purposes of the following calculations we can treat the earth as a single point. Therefore, the subsolar point
            // vector becomes the unit position vector from any point on earth to the sun.
            const solarZenithAngle = Math.acos(MathUtils.clamp(Vec3Math.dot(this.ppos, subSolarPoint), -1, 1));
            const horizonZenithAngle = MathUtils.HALF_PI + Math.acos(1 / (1 + UnitType.METER.convertTo(Math.max(0, this.altitude), UnitType.GA_RADIAN)));
            this._intensity.set(MathUtils.lerp(horizonZenithAngle - solarZenithAngle, BacklightLevelController.AUTO_MIN_SOLAR_HORIZON_ANGLE_RAD, BacklightLevelController.AUTO_MAX_SOLAR_HORIZON_ANGLE_RAD, this._autoMinIntensity, this._autoMaxIntensity, true, true));
        }
        /**
         * Calculates the subsolar point (the point on Earth's surface directly below the Sun, where solar zenith angle = 0)
         * given a specific time.
         * @param time A UNIX timestamp in milliseconds.
         * @param out A Float64Array object to which to write the result.
         * @returns The subsolar point at the specified time.
         */
        static calculateSubSolarPoint(time, out) {
            // Source: Zhang, T et al. https://doi.org/10.1016/j.renene.2021.03.047
            const PI2 = 2 * Math.PI;
            const days = (time - BacklightLevelController.EPOCH) / BacklightLevelController.DAY;
            const daysFrac = days - Math.floor(days);
            const L = (4.895055 + 0.01720279 * days);
            const g = (6.240041 + 0.01720197 * days);
            const lambda = L + 0.033423 * Math.sin(g) + 0.000349 * Math.sin(2 * g);
            const epsilon = 0.40910518 - 6.98e-9 * days;
            const rAscension = Math.atan2(Math.cos(epsilon) * Math.sin(lambda), Math.cos(lambda));
            const declination = Math.asin(Math.sin(epsilon) * Math.sin(lambda));
            // equation of time in days.
            const E = (((L - rAscension) % PI2 + 3 * Math.PI) % PI2 - Math.PI) * 0.159155;
            const lat = declination * Avionics.Utils.RAD2DEG;
            const lon = -15 * (daysFrac - 0.5 + E) * 24;
            return GeoPoint.sphericalToCartesian(lat, lon, out);
        }
    }
    /** The difference, in degrees, between horizon zenith angle and solar zenith angle at which auto backlight reaches maximum intensity. */
    BacklightLevelController.AUTO_MAX_SOLAR_HORIZON_ANGLE = 4;
    /** The difference, in degrees, between horizon zenith angle and solar zenith angle at which auto backlight reaches minimum intensity. */
    BacklightLevelController.AUTO_MIN_SOLAR_HORIZON_ANGLE = -6;
    BacklightLevelController.AUTO_MAX_SOLAR_HORIZON_ANGLE_RAD = BacklightLevelController.AUTO_MAX_SOLAR_HORIZON_ANGLE * Avionics.Utils.DEG2RAD;
    BacklightLevelController.AUTO_MIN_SOLAR_HORIZON_ANGLE_RAD = BacklightLevelController.AUTO_MIN_SOLAR_HORIZON_ANGLE * Avionics.Utils.DEG2RAD;
    BacklightLevelController.AUTO_UPDATE_REALTIME_FREQ = 10; // max frequency (Hz) of auto backlight level updates in real time
    BacklightLevelController.AUTO_UPDATE_SIMTIME_THRESHOLD = 60000; // minimum interval (ms) between auto backlight level updates in sim time
    BacklightLevelController.EPOCH = 946684800000; // Jan 1, 2000 00:00:00 UTC
    BacklightLevelController.DAY = 86400000; // milliseconds in one day
    BacklightLevelController.DEFAULT_MIN_INTENSITY = 0;
    BacklightLevelController.DEFAULT_MAX_INTENSITY = 1;
    BacklightLevelController.tempVec3 = new Float64Array(3);

    /** A class that linearly drives a SimVar value towards a given set point. */
    class LinearServo {
        /**
         * Creates an instance of a LinearServo.
         * @param rate The default rate to drive this servo, in units per second.
         */
        constructor(rate) {
            this.rate = rate;
        }
        /**
         * Drives this servo towards the set point.
         * @param currentValue The current value.
         * @param setValue The value to drive towards.
         * @param time The current timestamp, in milliseconds. Defaults to the current operating system time, as a Javascript
         * timestamp.
         * @param rate The rate to use to drive this servo, in units per second. Defaults to this servo's default rate.
         * @returns The output value.
         */
        drive(currentValue, setValue, time = Date.now(), rate = this.rate) {
            if (this.currentTime === undefined) {
                this.currentTime = time;
                return currentValue;
            }
            const currentTime = time;
            const deltaTime = currentTime - this.currentTime;
            this.currentTime = currentTime;
            const deltaValue = setValue - currentValue;
            const maximumDrive = rate * (deltaTime / 1000);
            const output = Math.abs(deltaValue) > maximumDrive
                ? currentValue + (Math.sign(deltaValue) * maximumDrive)
                : setValue;
            return output;
        }
        /**
         * Resets the servo to initial state
         */
        reset() {
            this.currentTime = undefined;
        }
    }

    // TODO: move to SDK?
    /** A class to simulate knob acceleration on value inputs */
    class InputAcceleration {
        /**
         * Ctor
         * @param options the knob acceleration settings
         * @param initiallyPaused whether the knob acceleration is initially paused
         */
        constructor(options = {}, initiallyPaused = false) {
            var _a;
            this.acceleration = 0;
            this.isPaused = false;
            this.maxAcceleration = 15;
            this.options = Object.assign({
                increment: 1,
                bigIncrement: ((_a = options.increment) !== null && _a !== void 0 ? _a : 1) * 10,
                accelDampeningPeriod: 50
            }, options);
            this.isPaused = initiallyPaused;
            if (!initiallyPaused) {
                this.resume();
            }
        }
        /** Updates the acceleration. */
        update() {
            // dampen acceleration
            if (this.acceleration > 0) {
                this.acceleration = MathUtils.clamp(this.acceleration - 1, 0, this.maxAcceleration);
                if (!this.isPaused) {
                    setTimeout(() => { this.update(); }, this.options.accelDampeningPeriod);
                }
            }
        }
        /**
         * Does a step and returns the increment value.
         * @returns the increment value
         */
        doStep() {
            this.acceleration += 2;
            const increment = (this.acceleration > 8 ? this.options.bigIncrement : this.options.increment);
            if (this.acceleration <= 2) {
                this.update();
            }
            return increment;
        }
        /** Pauses the update loop */
        pause() {
            this.isPaused = true;
        }
        /** Resumes the update loop */
        resume() {
            this.isPaused = false;
            this.acceleration = 0;
        }
    }

    /**
     * A bank angle controller that maintains a constant radius turn.
     */
    class ArcTurnController {
        constructor() {
            this.bankController = new PidController(1.5, 0, 0, 15, -15);
            this.precessionController = new PidController(0.025, 0, 0, 300, -300);
            this.filter = new ExpSmoother(500);
        }
        /**
         * Gets the bank angle output for a given radius error.
         * @param radiusError The radius error.
         * @returns The bank angle output.
         */
        getOutput(radiusError) {
            var _a;
            const currentTime = new Date().appTime();
            let bankAngle = 0;
            if (this.previousRadiusError !== undefined && this.previousTime !== undefined) {
                const dTime = currentTime - this.previousTime;
                const input = ((radiusError - this.previousRadiusError) / dTime) * 1000;
                const precessionRate = isNaN((_a = this.filter.last()) !== null && _a !== void 0 ? _a : NaN)
                    ? this.filter.reset(input)
                    : this.filter.next(input, dTime);
                const targetPrecessionRate = -this.precessionController.getOutput(dTime, radiusError);
                const precessionError = targetPrecessionRate - precessionRate;
                bankAngle = this.bankController.getOutput(dTime, precessionError);
            }
            this.previousTime = currentTime;
            this.previousRadiusError = radiusError;
            return -bankAngle;
        }
        /**
         * Resets the controller.
         */
        reset() {
            this.previousTime = undefined;
            this.previousRadiusError = undefined;
            this.precessionController.reset();
            this.bankController.reset();
            this.filter.reset();
        }
    }

    /**
     * A computer that generates roll-steering commands from LNAV steering commands.
     */
    class LNavRollSteerComputer {
        /**
         * Creates a new instance of LNavRollSteerComputer.
         * @param dataProvider A provider of data for this computer.
         * @param options Options with which to configure the computer.
         */
        constructor(dataProvider, options) {
            var _a;
            this.dataProvider = dataProvider;
            this.arcController = new ArcTurnController();
            this.steerCommandBuffer = [
                { isValid: false, isHeading: false, courseToSteer: 0, trackRadius: 0, dtk: 0, xtk: 0, tae: 0, desiredBankAngle: 0 },
                { isValid: false, isHeading: false, courseToSteer: 0, trackRadius: 0, dtk: 0, xtk: 0, tae: 0, desiredBankAngle: 0 }
            ];
            this._steerCommand = Subject.create(this.steerCommandBuffer[0], (a, b) => {
                if (!a.isValid && !b.isValid) {
                    return true;
                }
                return a.isValid === b.isValid
                    && a.isHeading === b.isHeading
                    && a.courseToSteer === b.courseToSteer
                    && a.trackRadius === b.trackRadius
                    && a.dtk === b.dtk
                    && a.xtk === b.xtk
                    && a.tae === b.tae;
            });
            /** The current roll steering command calculated by this computer. */
            this.steerCommand = this._steerCommand;
            this.lastUpdateTime = undefined;
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = () => 30;
            }
        }
        /**
         * Resets this computer.
         */
        reset() {
            this.arcController.reset();
            this.lastUpdateTime = undefined;
        }
        /**
         * Updates this computer.
         * @param time The current timestamp, in milliseconds.
         */
        update(time) {
            // TODO: make the arc controller use the time passed into update() instead of hardcoding it to use operating system
            // time (via Date()).
            const lnavSteerCommand = this.dataProvider.lnavSteerCommand.get();
            if (lnavSteerCommand.isValid) {
                const desiredBankAngle = this.getDesiredBankAngle();
                if (desiredBankAngle === undefined) {
                    this.arcController.reset();
                    this.setSteerCommand(lnavSteerCommand, false, 0);
                }
                else {
                    this.setSteerCommand(lnavSteerCommand, true, desiredBankAngle);
                }
            }
            else {
                this.arcController.reset();
                this.setSteerCommand(lnavSteerCommand, false, 0);
            }
            this.lastUpdateTime = time;
        }
        /**
         * Sets this computer's roll steering command.
         * @param lnavSteerCommand The LNAV steering command from which the roll steering command was generated.
         * @param isValid Whether the roll steering command is valid.
         * @param desiredBankAngle The command's desired bank angle, in degrees. Positive values indicate leftward bank.
         */
        setSteerCommand(lnavSteerCommand, isValid, desiredBankAngle) {
            const steerCommandBufferActiveIndex = this._steerCommand.get() === this.steerCommandBuffer[0] ? 0 : 1;
            const command = this.steerCommandBuffer[(steerCommandBufferActiveIndex + 1) % 2];
            command.isValid = isValid;
            command.isHeading = lnavSteerCommand.isHeading;
            command.courseToSteer = lnavSteerCommand.courseToSteer;
            command.trackRadius = lnavSteerCommand.trackRadius;
            command.dtk = lnavSteerCommand.dtk;
            command.xtk = lnavSteerCommand.xtk;
            command.tae = lnavSteerCommand.tae;
            command.desiredBankAngle = desiredBankAngle;
            this._steerCommand.set(command);
        }
        /**
         * Gets the desired bank angle, in degrees, for this computer's current LNAV steering command.
         * @returns The desired bank angle, in degrees, for this computer's current LNAV steering command, or `undefined` if
         * a bank angle could not be computed. Positive values indicate leftward bank.
         */
        getDesiredBankAngle() {
            const command = this.dataProvider.lnavSteerCommand.get();
            let trackToUse;
            let gsToUse = 0;
            if (command.isHeading) {
                const heading = this.dataProvider.heading.get();
                if (heading === null) {
                    return undefined;
                }
                trackToUse = heading;
            }
            else {
                const track = this.dataProvider.track.get();
                const gs = this.dataProvider.gs.get();
                if (track === null || gs === null) {
                    return undefined;
                }
                trackToUse = track;
                gsToUse = gs;
            }
            const maxBankAngle = this.maxBankAngleFunc();
            let desiredBankAngle = this.getDesiredBankAngleForTrack(command.courseToSteer, trackToUse, maxBankAngle);
            if (command.isHeading || command.trackRadius === MathUtils.HALF_PI) {
                this.arcController.reset();
            }
            else {
                desiredBankAngle = this.adjustDesiredBankAngleForTurn(command, desiredBankAngle, gsToUse, maxBankAngle);
            }
            return desiredBankAngle;
        }
        /**
         * Gets the desired bank angle, in degrees, to follow a great-circle track.
         * @param desiredTrack The desired track, in degrees.
         * @param track The plane's current track, in degrees.
         * @param maxBankAngle The maximum allowed bank angle, in degrees.
         * @returns The desired bank angle, in degrees, to follow the specified great-circle track. Positive values indicate
         * leftward bank.
         */
        getDesiredBankAngleForTrack(desiredTrack, track, maxBankAngle) {
            const turnDirection = NavMath.getTurnDirection(track, desiredTrack);
            const headingDiff = MathUtils.angularDistanceDeg(track, desiredTrack, 0);
            let baseBank = Math.min(1.25 * headingDiff, maxBankAngle);
            baseBank *= (turnDirection === 'left' ? 1 : -1);
            return baseBank;
        }
        /**
         * Adjusts a desired bank angle to follow a turning path defined by an LNAV steering command.
         * @param command The LNAV steering command that defines the turning path to follow.
         * @param desiredBankAngle The desired bank angle, in degrees, before adjusting for the turning path. Positive values
         * indicate leftward bank.
         * @param gs The plane's current ground speed, in knots.
         * @param maxBankAngle The maximum allowed bank angle, in degrees.
         * @returns The adjusted desired bank angle to follow the specified turning path, in degrees. Positive values
         * indicate leftward bank.
         */
        adjustDesiredBankAngleForTurn(command, desiredBankAngle, gs, maxBankAngle) {
            const trackTurnRadiusMeters = UnitType.GA_RADIAN.convertTo(Math.min(command.trackRadius, Math.PI - command.trackRadius), UnitType.METER);
            const xtkMeters = UnitType.NMILE.convertTo(command.xtk, UnitType.METER);
            const bankAdjustment = this.arcController.getOutput(xtkMeters);
            const turnBankAngle = NavMath.bankAngle(gs, trackTurnRadiusMeters) * (command.trackRadius < MathUtils.HALF_PI ? 1 : -1);
            const turnRadius = NavMath.turnRadius(gs, maxBankAngle);
            const bankBlendFactor = Math.max(1 - (Math.abs(xtkMeters) / turnRadius), 0);
            return MathUtils.clamp((desiredBankAngle * (1 - bankBlendFactor)) + (turnBankAngle * bankBlendFactor) + bankAdjustment, -maxBankAngle, maxBankAngle);
        }
    }

    /**
     * An autopilot GPS roll-steering director. This director converts GPS steering commands into roll-steering commands
     * in order to drive flight director bank commands. This director also sets the `AUTOPILOT NAV1 LOCK` SimVar state to
     * true (1) when it is armed or activated, and to false (0) when it is deactivated.
     */
    class APGpsSteerDirector {
        /**
         * Creates a new instance of APGpsSteerDirector.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param steerCommand The steering command used by this director.
         * @param options Options to configure the new director.
         */
        constructor(apValues, steerCommand, options) {
            var _a, _b, _c, _d, _e;
            this.apValues = apValues;
            this.steerCommand = steerCommand;
            this.isNavLock = Subject.create(false);
            this.callbackState = {
                gpsSteerCommand: this.steerCommand.get(),
                rollSteerCommand: null
            };
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.canArmFunc = (_b = options === null || options === void 0 ? void 0 : options.canArm) !== null && _b !== void 0 ? _b : ((apValuesInner, state) => state.gpsSteerCommand.isValid);
            this.canRemainArmedFunc = (_c = options === null || options === void 0 ? void 0 : options.canRemainArmed) !== null && _c !== void 0 ? _c : ((apValuesInner, state) => state.gpsSteerCommand.isValid);
            this.canActivateFunc = (_d = options === null || options === void 0 ? void 0 : options.canActivate) !== null && _d !== void 0 ? _d : ((apValuesInner, state) => state.rollSteerCommand !== null && state.rollSteerCommand.isValid);
            this.canRemainActiveFunc = (_e = options === null || options === void 0 ? void 0 : options.canRemainActive) !== null && _e !== void 0 ? _e : ((apValuesInner, state) => {
                var _a, _b;
                return state.rollSteerCommand !== null
                    && state.rollSteerCommand.isValid
                    && (((_a = apValuesInner.cdiSource.get()) === null || _a === void 0 ? void 0 : _a.type) === exports.NavSourceType.Gps
                        || !!((_b = apValuesInner.navToNavTransferInProgress) === null || _b === void 0 ? void 0 : _b.call(apValuesInner)));
            });
            this.lnavRollSteerComputerDataProvider = new DirectorLNavRollSteerComputerDataProvider(this.steerCommand);
            this.lnavRollSteerComputer = new LNavRollSteerComputer(this.lnavRollSteerComputerDataProvider, {
                maxBankAngle: this.maxBankAngleFunc
            });
            this.isNavLock.sub(newState => {
                SimVar.SetSimVarValue('AUTOPILOT NAV1 LOCK', exports.SimVarValueType.Bool, newState);
            });
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritDoc */
        activate() {
            this.state = exports.DirectorState.Active;
            this.onActivate && this.onActivate();
            this.isNavLock.set(true);
        }
        /** @inheritDoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.updateCallbackState(null);
                if (this.canArmFunc(this.apValues, this.callbackState)) {
                    this.state = exports.DirectorState.Armed;
                    this.onArm && this.onArm();
                    this.isNavLock.set(true);
                }
            }
        }
        /** @inheritDoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.onDeactivate && this.onDeactivate();
            this.isNavLock.set(false);
            this.lnavRollSteerComputer.reset();
        }
        /** @inheritDoc */
        update() {
            this.lnavRollSteerComputerDataProvider.update();
            this.lnavRollSteerComputer.update(Date.now());
            this.updateCallbackState(this.lnavRollSteerComputer.steerCommand.get());
            if (this.state === exports.DirectorState.Armed) {
                if (!this.canRemainArmedFunc(this.apValues, this.callbackState)) {
                    this.deactivate();
                    return;
                }
                else {
                    this.tryActivate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                if (!this.canRemainActiveFunc(this.apValues, this.callbackState)) {
                    this.deactivate();
                    return;
                }
                else {
                    const rollSteerCommand = this.lnavRollSteerComputer.steerCommand.get();
                    if (rollSteerCommand.isValid && isFinite(rollSteerCommand.desiredBankAngle)) {
                        this.driveBankFunc(rollSteerCommand.desiredBankAngle);
                    }
                }
            }
        }
        /**
         * Updates this director's callback state.
         * @param rollSteerCommand The roll steering command to write to the callback state.
         */
        updateCallbackState(rollSteerCommand) {
            this.callbackState.gpsSteerCommand = this.steerCommand.get();
            this.callbackState.rollSteerCommand = rollSteerCommand;
        }
        /**
         * Attempts to activate this director from an armed state.
         */
        tryActivate() {
            if (this.canActivateFunc(this.apValues, this.callbackState)) {
                this.activate();
            }
        }
    }
    /**
     * An implementation of {@link LNavRollSteerComputerDataProvider} used by {@link APGpsSteerDirector}.
     */
    class DirectorLNavRollSteerComputerDataProvider {
        /**
         * Creates a new instance of DirectorLNavRollSteerComputerDataProvider.
         * @param steerCommand The LNAV steering command from which this provider sources data.
         */
        constructor(steerCommand) {
            // TODO: Move this stuff into a central data provider for the autopilot.
            this.velocityXSimVarId = SimVar.GetRegisteredId('VELOCITY WORLD X', exports.SimVarValueType.Knots, '');
            this.velocityZSimVarId = SimVar.GetRegisteredId('VELOCITY WORLD Z', exports.SimVarValueType.Knots, '');
            this.headingSimVarId = SimVar.GetRegisteredId('PLANE HEADING DEGREES TRUE', exports.SimVarValueType.Degree, '');
            this._gs = Value.create(null);
            /** @inheritDoc */
            this.gs = this._gs;
            this._track = Value.create(null);
            /** @inheritDoc */
            this.track = this._track;
            this._heading = Value.create(null);
            /** @inheritDoc */
            this.heading = this._heading;
            this.lnavSteerCommand = steerCommand;
        }
        /**
         * Updates this provider's data.
         */
        update() {
            let heading;
            const velocityEW = SimVar.GetSimVarValueFastReg(this.velocityXSimVarId);
            const velocityNS = SimVar.GetSimVarValueFastReg(this.velocityZSimVarId);
            const gs = Math.hypot(velocityEW, velocityNS);
            this._gs.set(gs);
            if (gs > 1) {
                this._track.set(MathUtils.normalizeAngleDeg(Math.atan2(velocityEW, velocityNS) * Avionics.Utils.RAD2DEG));
            }
            else {
                this._track.set(heading = SimVar.GetSimVarValueFastReg(this.headingSimVarId));
            }
            this._heading.set(heading !== null && heading !== void 0 ? heading : SimVar.GetSimVarValueFastReg(this.headingSimVarId));
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * An autopilot director that provides vertical guidance by tracking a glideslope signal from a radio navigation aid.
     *
     * Requires that the navigation radio topics defined in {@link NavComEvents} be published to the event bus in order to
     * function properly.
     */
    class APGSDirector {
        /**
         * Creates a new instance of APGSDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options with which to configure the director.
         */
        constructor(bus, apValues, options) {
            var _a, _b, _c, _d, _e, _f;
            this.bus = bus;
            this.apValues = apValues;
            this.navSource = {
                index: 0,
                type: exports.NavSourceType.Nav,
            };
            this.navFrequency = ConsumerValue.create(null, 0);
            this.navSignal = ConsumerValue.create(null, 0);
            this.navHasGs = ConsumerValue.create(null, false);
            this.navGsAngle = ConsumerValue.create(null, 0);
            this.navGsError = ConsumerValue.create(null, null);
            this.navLla = ConsumerValue.create(null, null);
            this.navData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0,
                signal: 0,
                hasGs: false,
                gsAngle: null,
                gsAngleError: null
            };
            this.activateNavData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0
            };
            this.state = exports.DirectorState.Inactive;
            this.forceNavSource = options === null || options === void 0 ? void 0 : options.forceNavSource;
            this.canArm = (_a = options === null || options === void 0 ? void 0 : options.canArm) !== null && _a !== void 0 ? _a : APGSDirector.defaultCanArm;
            this.canActivate = (_b = options === null || options === void 0 ? void 0 : options.canActivate) !== null && _b !== void 0 ? _b : APGSDirector.defaultCanActivate;
            this.canRemainActive = (_c = options === null || options === void 0 ? void 0 : options.canRemainActive) !== null && _c !== void 0 ? _c : APGSDirector.defaultCanRemainActive;
            this.angleClosureRateFunc = (_d = options === null || options === void 0 ? void 0 : options.angleClosureRate) !== null && _d !== void 0 ? _d : APGSDirector.defaultAngleClosureRate;
            this.vsTargetFunc = options === null || options === void 0 ? void 0 : options.vsTarget;
            const vsUnit = this.vsTargetFunc ? UnitType.FPM : UnitType.MPS;
            this.minVs = UnitType.FPM.convertTo((_e = options === null || options === void 0 ? void 0 : options.minVs) !== null && _e !== void 0 ? _e : -3000, vsUnit);
            this.maxVs = UnitType.FPM.convertTo((_f = options === null || options === void 0 ? void 0 : options.maxVs) !== null && _f !== void 0 ? _f : 0, vsUnit);
            this.initCdiSourceSubs();
        }
        /**
         * Initializes this director's subscription to the autopilot's CDI source. If this director is forced to use a
         * specific CDI source, then the autopilot's CDI source will be ignored.
         */
        initCdiSourceSubs() {
            if (this.forceNavSource !== undefined) {
                this.onCdiSourceChanged({
                    index: this.forceNavSource,
                    type: exports.NavSourceType.Nav,
                });
            }
            else {
                this.apValues.cdiSource.sub(this.onCdiSourceChanged.bind(this), true);
            }
        }
        /**
         * Responds to when the CDI source used by this director changes.
         * @param source The new CDI source to use.
         */
        onCdiSourceChanged(source) {
            Object.assign(this.navSource, source);
            if (source.type === exports.NavSourceType.Nav && source.index >= 1 && source.index <= 4) {
                const index = source.index;
                const sub = this.bus.getSubscriber();
                this.navFrequency.setConsumerWithDefault(sub.on(`nav_active_frequency_${index}`), 0);
                this.navSignal.setConsumerWithDefault(sub.on(`nav_signal_${index}`), 0);
                this.navHasGs.setConsumerWithDefault(sub.on(`nav_glideslope_${index}`), false);
                this.navGsAngle.setConsumerWithDefault(sub.on(`nav_raw_gs_${index}`), 0);
                this.navGsError.setConsumerWithDefault(sub.on(`nav_gs_error_${index}`), 0);
                this.navLla.setConsumerWithDefault(sub.on(`nav_lla_${index}`), null);
            }
            else {
                this.navFrequency.reset(0);
                this.navSignal.reset(0);
                this.navHasGs.reset(false);
                this.navGsAngle.reset(0);
                this.navGsError.reset(null);
                this.navLla.reset(null);
            }
        }
        /**
         * Updates this director's radio navigation data.
         */
        updateNavData() {
            Object.assign(this.navData.navSource, this.navSource);
            this.navData.frequency = this.navFrequency.get();
            this.navData.signal = this.navSignal.get();
            this.navData.hasGs = this.navHasGs.get();
            this.navData.gsAngle = this.navData.hasGs ? this.navGsAngle.get() : null;
            this.navData.gsAngleError = this.navData.signal > 0 && this.navData.hasGs ? this.navGsError.get() : null;
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.GSActive);
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', false);
            Object.assign(this.activateNavData.navSource, this.navSource);
            this.activateNavData.frequency = this.navFrequency.get();
        }
        /**
         * Arms this director.
         */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.updateNavData();
                if (this.canArm(this.apValues, this.navData)) {
                    this.state = exports.DirectorState.Armed;
                    SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.GSArmed);
                    if (this.onArm !== undefined) {
                        this.onArm();
                    }
                    SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', true);
                    SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', false);
                    SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
                }
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue(exports.VNavVars.GPApproachMode, exports.SimVarValueType.Number, exports.ApproachGuidanceMode.None);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ARM', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT GLIDESLOPE ACTIVE', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', false);
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Armed) {
                this.updateNavData();
                if (this.canActivate(this.apValues, this.navData)) {
                    this.activate();
                }
                if (!this.canArm(this.apValues, this.navData)) {
                    this.deactivate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                this.updateNavData();
                if (!this.canRemainActive(this.apValues, this.navData, this.activateNavData)) {
                    this.deactivate();
                }
                this.trackGlideslope();
            }
        }
        /**
         * Tracks the Glideslope.
         */
        trackGlideslope() {
            const gsError = this.navHasGs.get() && this.navSignal.get() > 0 ? this.navGsError.get() : null;
            const navLla = this.navLla.get();
            if (gsError !== null && navLla !== null) {
                const gsAngle = this.navGsAngle.get();
                const distanceM = UnitType.GA_RADIAN.convertTo(GeoPoint.distance(navLla.lat, navLla.long, SimVar.GetSimVarValue('PLANE LATITUDE', exports.SimVarValueType.Degree), SimVar.GetSimVarValue('PLANE LONGITUDE', exports.SimVarValueType.Degree)), UnitType.METER);
                // We want the height of the plane above the glideslope antenna, which we can calculate from distance,
                // glideslope angle, and glideslope error.
                const heightM = distanceM * Math.tan((gsAngle + gsError) * Avionics.Utils.DEG2RAD);
                const groundSpeedMps = SimVar.GetSimVarValue('GROUND VELOCITY', exports.SimVarValueType.MetersPerSecond);
                const vsMps = SimVar.GetSimVarValue('VERTICAL SPEED', exports.SimVarValueType.MetersPerSecond);
                const hypotSq = distanceM * distanceM + heightM * heightM;
                const heightTimesGs = heightM * groundSpeedMps;
                const currentAngleRate = (vsMps * distanceM + heightTimesGs) / hypotSq;
                let targetVs;
                let unit;
                if (this.vsTargetFunc) {
                    targetVs = this.vsTargetFunc(gsError, gsAngle, currentAngleRate, distanceM, heightM, groundSpeedMps, vsMps);
                    unit = exports.SimVarValueType.FPM;
                }
                else {
                    const desiredClosureRate = this.angleClosureRateFunc(gsError, gsAngle, currentAngleRate, distanceM, heightM, groundSpeedMps, vsMps);
                    const desiredAngleRate = Math.sign(gsError) * -1 * desiredClosureRate;
                    targetVs = (Avionics.Utils.DEG2RAD * desiredAngleRate * hypotSq - heightTimesGs) / distanceM;
                    unit = exports.SimVarValueType.MetersPerSecond;
                }
                targetVs = MathUtils.clamp(targetVs, this.minVs, this.maxVs);
                const pitchForVerticalSpeed = Math.asin(MathUtils.clamp(targetVs / SimVar.GetSimVarValue('AIRSPEED TRUE', unit), -1, 1)) * Avionics.Utils.RAD2DEG;
                const targetPitch = MathUtils.clamp(pitchForVerticalSpeed, -8, 3);
                this.drivePitch && this.drivePitch(-targetPitch, true, true);
            }
        }
        /**
         * A default function that checks whether the director can be armed.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be armed.
         */
        static defaultCanArm(apValues, navData) {
            return (apValues.navToNavArmableVerticalMode && apValues.navToNavArmableVerticalMode() === exports.APVerticalModes.GS)
                || navData.hasGs;
        }
        /**
         * A default function that checks whether the director can be activated from an armed state.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be activated from an armed state.
         */
        static defaultCanActivate(apValues, navData) {
            return apValues.lateralActive.get() === exports.APLateralModes.LOC
                && navData.gsAngleError !== null
                && Math.abs(navData.gsAngleError) <= 0.1;
        }
        /**
         * A default function that checks whether the director can remain in the active state.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can remain in the active state.
         */
        static defaultCanRemainActive(apValues, navData) {
            return apValues.lateralActive.get() === exports.APLateralModes.LOC && navData.gsAngleError !== null;
        }
        /**
         * A default function which calculates a desired angle closure rate, in degrees per second, to track a glideslope. The angle
         * closure rate is the rate of reduction of glideslope angle error. Positive values reduce glideslope angle error while
         * negative values increase glideslope angle error.
         * @param gsAngleError The glideslope angle error, in degrees, defined as the difference between the angle from the
         * glideslope antenna to the airplane and the glideslope angle. Positive values indicate deviation of the airplane
         * above the glideslope.
         * @returns The desired angle closure rate, in degrees per second, toward the glideslope.
         */
        static defaultAngleClosureRate(gsAngleError) {
            // We will target 0.1 degrees per second by default at full-scale deviation, decreasing linearly down to 0 at no
            // deviation. This is equivalent to a constant time-to-intercept of 7 seconds at full-scale deviation or less.
            return MathUtils.lerp(Math.abs(gsAngleError), 0, 0.7, 0, 0.1, true, true);
        }
    }

    /**
     * A heading autopilot director.
     */
    class APHdgDirector {
        /**
         * Creates a new instance of APHdgDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director.
         */
        constructor(bus, apValues, options) {
            var _a, _b, _c;
            this.apValues = apValues;
            this.currentHeading = ConsumerValue.create(null, 0);
            this.toGaHeading = 0;
            this.lastHeadingDiff = undefined;
            this.lockedTurnDirection = undefined;
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.turnReversalThreshold = (_b = options === null || options === void 0 ? void 0 : options.turnReversalThreshold) !== null && _b !== void 0 ? _b : 0;
            this.isToGaMode = (_c = options === null || options === void 0 ? void 0 : options.isToGaMode) !== null && _c !== void 0 ? _c : false;
            this.currentHeading.setConsumer(bus.getSubscriber().on('hdg_deg').withPrecision(1));
            this.pauseSubs();
            this.state = exports.DirectorState.Inactive;
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.currentHeading.resume();
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.currentHeading.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            if (!this.isToGaMode) {
                SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', true);
            }
            else {
                this.toGaHeading = this.currentHeading.get();
            }
            this.state = exports.DirectorState.Active;
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        async deactivate() {
            this.pauseSubs();
            if (!this.isToGaMode) {
                await SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', false);
            }
            this.state = exports.DirectorState.Inactive;
            this.lastHeadingDiff = undefined;
            this.lockedTurnDirection = undefined;
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                let bank;
                if (this.isToGaMode) {
                    if (Simplane.getIsGrounded()) {
                        this.toGaHeading = this.currentHeading.get();
                    }
                    bank = this.desiredBank(this.toGaHeading);
                }
                else {
                    bank = this.desiredBank(this.apValues.selectedHeading.get());
                }
                this.driveBankFunc(bank);
            }
        }
        /**
         * Gets a desired bank from a Target Selected Heading.
         * @param targetHeading The target heading.
         * @returns The desired bank angle.
         */
        desiredBank(targetHeading) {
            const currentHeading = this.currentHeading.get();
            const headingDiff = MathUtils.diffAngleDeg(currentHeading, targetHeading);
            let turnDirection = undefined;
            let directionalHeadingDiff;
            if (this.lockedTurnDirection !== undefined) {
                turnDirection = this.lockedTurnDirection;
                directionalHeadingDiff = turnDirection === 'left' ? (360 - headingDiff) % 360 : headingDiff;
                if (directionalHeadingDiff >= this.turnReversalThreshold) {
                    turnDirection = undefined;
                }
                else if (this.lastHeadingDiff !== undefined) {
                    // Check if the heading difference passed through zero in the positive to negative direction since the last
                    // update. If so, we may need to issue a turn reversal.
                    const headingDiffDelta = (MathUtils.diffAngleDeg(this.lastHeadingDiff, directionalHeadingDiff) + 180) % 360 - 180; // -180 to +180
                    if (this.lastHeadingDiff + headingDiffDelta < 0) {
                        turnDirection = undefined;
                    }
                }
            }
            if (turnDirection === undefined) {
                turnDirection = NavMath.getTurnDirection(currentHeading, targetHeading);
                directionalHeadingDiff = turnDirection === 'left' ? (360 - headingDiff) % 360 : headingDiff;
            }
            if (this.turnReversalThreshold > 180) {
                this.lockedTurnDirection = turnDirection;
            }
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            this.lastHeadingDiff = directionalHeadingDiff;
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            let baseBank = Math.min(1.25 * directionalHeadingDiff, this.maxBankAngleFunc());
            baseBank *= (turnDirection === 'left' ? 1 : -1);
            return baseBank;
        }
    }

    /**
     * An autopilot heading hold director.
     * Levels the wings upon activation, and then holds the captured heading
     */
    class APHdgHoldDirector {
        /**
         * Creates an instance of the heading hold director.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director.
         */
        constructor(bus, apValues, options) {
            var _a;
            this.apValues = apValues;
            this.currentHeading = 0;
            this.currentBank = 0;
            /** heading captured at wings level, or null if not yet captured */
            this.capturedHeading = null;
            this.currentBankSub = bus.getSubscriber().on('roll_deg').withPrecision(1).handle((bank) => this.currentBank = bank);
            this.currentHeadingSub = bus.getSubscriber().on('hdg_deg').withPrecision(0).handle((h) => this.currentHeading = h);
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.state = exports.DirectorState.Inactive;
            this.pauseSubs();
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.currentHeadingSub.resume(true);
            this.currentBankSub.resume(true);
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.currentHeadingSub.pause();
            this.currentBankSub.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            this.state = exports.DirectorState.Active;
            this.capturedHeading = null;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', false);
            this.pauseSubs();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                if (this.capturedHeading === null && Math.abs(this.currentBank) < APHdgHoldDirector.MIN_BANK_THRESHOLD) {
                    this.capturedHeading = this.currentHeading;
                }
                this.driveBankFunc(this.capturedHeading !== null ? this.desiredBank(this.capturedHeading) : 0);
            }
        }
        /**
         * Gets a desired bank from a Target Selected Heading.
         * @param targetHeading The target heading.
         * @returns The desired bank angle.
         */
        desiredBank(targetHeading) {
            const turnDirection = NavMath.getTurnDirection(this.currentHeading, targetHeading);
            const headingDiff = Math.abs(NavMath.diffAngle(this.currentHeading, targetHeading));
            let baseBank = Math.min(1.25 * headingDiff, this.maxBankAngleFunc());
            baseBank *= (turnDirection === 'left' ? 1 : -1);
            return baseBank;
        }
    }
    /** bank angle below which we capture the heading */
    APHdgHoldDirector.MIN_BANK_THRESHOLD = 1;

    /**
     * Phases used by {@link APNavDirector} when active and tracking a navigation signal.
     */
    exports.APNavDirectorPhase = void 0;
    (function (APNavDirectorPhase) {
        APNavDirectorPhase["Intercept"] = "Intercept";
        APNavDirectorPhase["Tracking"] = "Tracking";
    })(exports.APNavDirectorPhase || (exports.APNavDirectorPhase = {}));
    /**
     * An autopilot director that provides lateral guidance by tracking a signal from a VOR or localizer radio navigation
     * aid.
     *
     * Requires that the navigation radio topics defined in {@link NavComEvents} be published to the event bus in order to
     * function properly.
     */
    class APNavDirector {
        /**
         * Creates a new instance of APNavDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param mode The APLateralMode for this instance of the director.
         * @param options Options with which to configure the director.
         */
        constructor(bus, apValues, mode, options) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
            var _0, _1, _2, _3, _4, _5, _6, _7;
            this.bus = bus;
            this.apValues = apValues;
            this.mode = mode;
            this.navSource = {
                index: 0,
                type: exports.NavSourceType.Nav,
            };
            this.deviationSimVar = 'NAV CDI:1';
            this.radialErrorSimVar = 'NAV RADIAL ERROR:1';
            this.navFrequency = ConsumerValue.create(null, 0);
            this.navObs = ConsumerValue.create(null, null);
            this.navSignal = ConsumerValue.create(null, 0);
            this.navHasNav = ConsumerValue.create(null, false);
            this.navCdi = ConsumerValue.create(null, null);
            this.navHasLoc = ConsumerValue.create(null, false);
            this.navLocCourse = ConsumerValue.create(null, 0);
            this.navMagVar = ConsumerValue.create(null, 0);
            this.navLla = ConsumerValue.create(null, null);
            this.isNavLock = Subject.create(false);
            this.navData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0,
                obsCourse: null,
                signal: 0,
                hasNav: false,
                hasLoc: false,
                locCourse: null,
                deviation: null
            };
            this.activateNavData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0,
                obsCourse: null
            };
            this.phase = undefined;
            this.lastPhaseUpdateTime = undefined;
            this.phaseTransitionTimer = 0;
            this.state = exports.DirectorState.Inactive;
            const interceptParams = {};
            const trackingParams = {};
            const maxBankAngleFunc = (_a = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngle)) !== null && _a !== void 0 ? _a : this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            interceptParams.maxBankAngleFunc = (_b = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngleIntercept)) !== null && _b !== void 0 ? _b : maxBankAngleFunc;
            trackingParams.maxBankAngleFunc = (_c = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngleTracking)) !== null && _c !== void 0 ? _c : maxBankAngleFunc;
            const driveBankFunc = (_d = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRate)) !== null && _d !== void 0 ? _d : ((bank) => {
                if (isFinite(bank) && this.driveBank) {
                    this.driveBank(bank);
                }
            });
            interceptParams.driveBankFunc = (_e = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRateIntercept)) !== null && _e !== void 0 ? _e : driveBankFunc;
            trackingParams.driveBankFunc = (_f = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRateTracking)) !== null && _f !== void 0 ? _f : driveBankFunc;
            this.canArm = (_g = options === null || options === void 0 ? void 0 : options.canArm) !== null && _g !== void 0 ? _g : APNavDirector.defaultCanArm;
            this.canActivate = (_h = options === null || options === void 0 ? void 0 : options.canActivate) !== null && _h !== void 0 ? _h : APNavDirector.defaultCanActivate;
            this.canRemainActive = (_j = options === null || options === void 0 ? void 0 : options.canRemainActive) !== null && _j !== void 0 ? _j : APNavDirector.defaultCanRemainActive;
            this.phaseOptions = Object.assign({}, options === null || options === void 0 ? void 0 : options.phaseOptions);
            (_k = (_0 = this.phaseOptions).interceptDeflection) !== null && _k !== void 0 ? _k : (_0.interceptDeflection = 0.25);
            (_l = (_1 = this.phaseOptions).interceptXtk) !== null && _l !== void 0 ? _l : (_1.interceptXtk = Infinity);
            (_m = (_2 = this.phaseOptions).interceptCourseError) !== null && _m !== void 0 ? _m : (_2.interceptCourseError = 5);
            (_o = (_3 = this.phaseOptions).interceptSwitchDelay) !== null && _o !== void 0 ? _o : (_3.interceptSwitchDelay = 5000);
            (_p = (_4 = this.phaseOptions).trackingDeflection) !== null && _p !== void 0 ? _p : (_4.trackingDeflection = 0.125);
            (_q = (_5 = this.phaseOptions).trackingXtk) !== null && _q !== void 0 ? _q : (_5.trackingXtk = Infinity);
            (_r = (_6 = this.phaseOptions).trackingCourseError) !== null && _r !== void 0 ? _r : (_6.trackingCourseError = 2);
            (_s = (_7 = this.phaseOptions).trackingSwitchDelay) !== null && _s !== void 0 ? _s : (_7.trackingSwitchDelay = 5000);
            this.phaseSelectorFunc = (_t = options === null || options === void 0 ? void 0 : options.phaseSelector) !== null && _t !== void 0 ? _t : this.defaultSelectPhase.bind(this);
            const lateralInterceptCurveFunc = (_u = options === null || options === void 0 ? void 0 : options.lateralInterceptCurve) !== null && _u !== void 0 ? _u : APNavDirector.defaultLateralInterceptCurve;
            interceptParams.lateralInterceptCurveFunc = (_v = options === null || options === void 0 ? void 0 : options.lateralInterceptCurveIntercept) !== null && _v !== void 0 ? _v : lateralInterceptCurveFunc;
            trackingParams.lateralInterceptCurveFunc = (_w = options === null || options === void 0 ? void 0 : options.lateralInterceptCurveTracking) !== null && _w !== void 0 ? _w : lateralInterceptCurveFunc;
            interceptParams.desiredBankFunc = (_x = options === null || options === void 0 ? void 0 : options.desiredBankIntercept) !== null && _x !== void 0 ? _x : ((trackError) => -trackError * 2);
            trackingParams.desiredBankFunc = (_y = options === null || options === void 0 ? void 0 : options.desiredBankTracking) !== null && _y !== void 0 ? _y : ((trackError) => -trackError * 1.25);
            this.phaseParameters = {
                [exports.APNavDirectorPhase.Intercept]: interceptParams,
                [exports.APNavDirectorPhase.Tracking]: trackingParams
            };
            this.disableArming = (_z = options === null || options === void 0 ? void 0 : options.disableArming) !== null && _z !== void 0 ? _z : false;
            this.forceNavSource = options === null || options === void 0 ? void 0 : options.forceNavSource;
            this.isNavLock.sub((newState) => {
                if (SimVar.GetSimVarValue('AUTOPILOT NAV1 LOCK', 'Bool') !== newState) {
                    SimVar.SetSimVarValue('AUTOPILOT NAV1 LOCK', 'Bool', newState);
                }
            });
            this.initCdiSourceSubs();
        }
        /**
         * Resolves a maximum bank angle option to a function which returns a maximum bank angle.
         * @param opt The option to resolve.
         * @returns A function which returns a maximum bank angle as defined by the specified option, or `undefined` if the
         * option is not defined.
         */
        resolveMaxBankAngleOption(opt) {
            switch (typeof opt) {
                case 'number':
                    return () => opt;
                case 'function':
                    return opt;
                default:
                    return undefined;
            }
        }
        /**
         * Resolves a bank rate option to a function which drives the commanded bank angle.
         * @param opt The option to resolve.
         * @returns A function which drives the commanded bank angle in accordance with the specified bank rate option, or
         * `undefined` if the option is not defined.
         */
        resolveBankRateOption(opt) {
            switch (typeof opt) {
                case 'number':
                    return bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, opt * this.apValues.simRate.get());
                        }
                    };
                case 'function':
                    return bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, opt() * this.apValues.simRate.get());
                        }
                    };
                default:
                    return undefined;
            }
        }
        /**
         * Initializes this director's subscription to the autopilot's CDI source. If this director is forced to use a
         * specific CDI source, then the autopilot's CDI source will be ignored.
         */
        initCdiSourceSubs() {
            if (this.forceNavSource !== undefined) {
                this.onCdiSourceChanged({
                    index: this.forceNavSource,
                    type: exports.NavSourceType.Nav,
                });
            }
            else {
                this.navSource = {
                    index: 0,
                    type: exports.NavSourceType.Nav,
                };
                this.apValues.cdiSource.sub(this.onCdiSourceChanged.bind(this), true);
            }
        }
        /**
         * Responds to when the CDI source used by this director changes.
         * @param source The new CDI source to use.
         */
        onCdiSourceChanged(source) {
            Object.assign(this.navSource, source);
            if (source.type === exports.NavSourceType.Nav && source.index >= 1 && source.index <= 4) {
                const index = source.index;
                this.deviationSimVar = `NAV CDI:${index}`;
                this.radialErrorSimVar = `NAV RADIAL ERROR:${index}`;
                const sub = this.bus.getSubscriber();
                this.navFrequency.setConsumerWithDefault(sub.on(`nav_active_frequency_${index}`), 0);
                this.navObs.setConsumerWithDefault(sub.on(`nav_obs_${index}`), null);
                this.navSignal.setConsumerWithDefault(sub.on(`nav_signal_${index}`), 0);
                this.navHasNav.setConsumerWithDefault(sub.on(`nav_has_nav_${index}`), false);
                this.navCdi.setConsumerWithDefault(sub.on(`nav_cdi_${index}`), null);
                this.navHasLoc.setConsumerWithDefault(sub.on(`nav_localizer_${index}`), false);
                this.navLocCourse.setConsumerWithDefault(sub.on(`nav_localizer_crs_${index}`), 0);
                this.navMagVar.setConsumerWithDefault(sub.on(`nav_magvar_${index}`), 0);
                this.navLla.setConsumerWithDefault(sub.on(`nav_lla_${index}`), null);
            }
            else {
                this.navFrequency.reset(0);
                this.navObs.reset(null);
                this.navSignal.reset(0);
                this.navHasNav.reset(false);
                this.navCdi.reset(null);
                this.navHasLoc.reset(false);
                this.navLocCourse.reset(0);
                this.navMagVar.reset(0);
                this.navLla.reset(null);
            }
        }
        /**
         * Updates this director's radio navigation data.
         */
        updateNavData() {
            Object.assign(this.navData.navSource, this.navSource);
            this.navData.frequency = this.navFrequency.get();
            this.navData.signal = this.navSignal.get();
            this.navData.obsCourse = this.navObs.get();
            this.navData.hasNav = this.navHasNav.get();
            this.navData.hasLoc = this.navHasLoc.get();
            this.navData.locCourse = this.navData.hasLoc ? this.navLocCourse.get() * Avionics.Utils.RAD2DEG : null;
            this.navData.deviation = this.navData.signal > 0 && this.navData.hasNav ? this.navCdi.get() : null;
            if (this.navData.deviation !== null) {
                this.navData.deviation /= 127;
            }
        }
        /**
         * Activates this director.
         */
        activate() {
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            this.setNavLock(true);
            this.state = exports.DirectorState.Active;
            Object.assign(this.activateNavData.navSource, this.navSource);
            this.activateNavData.frequency = this.navFrequency.get();
            this.activateNavData.obsCourse = this.navObs.get();
        }
        /**
         * Arms this director.
         */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.updateNavData();
                if (this.canArm(this.mode, this.apValues, this.navData)) {
                    this.state = exports.DirectorState.Armed;
                    if (this.onArm !== undefined) {
                        this.onArm();
                    }
                    this.setNavLock(true);
                }
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.phase = undefined;
            this.setNavLock(false);
        }
        /**
         * Sets the NAV1 Lock state.
         * @param newState The new state of the NAV1 lock.
         */
        setNavLock(newState) {
            this.isNavLock.set(newState);
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Armed) {
                this.updateNavData();
                if (!this.canArm(this.mode, this.apValues, this.navData)) {
                    this.deactivate();
                }
                else if (this.disableArming || this.canActivate(this.mode, this.apValues, this.navData)) {
                    this.activate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                this.updateNavData();
                const distanceToSource = this.getNavDistance();
                const isInZoneOfConfusion = !this.navHasLoc.get() && this.isInZoneOfConfusion(distanceToSource);
                if (!this.canRemainActive(this.mode, this.apValues, this.navData, isInZoneOfConfusion, this.activateNavData)) {
                    this.deactivate();
                }
                else {
                    this.trackSignal(distanceToSource, isInZoneOfConfusion);
                }
            }
        }
        /**
         * Tracks the active navigation signal received by this director.
         * @param distanceToSource The distance to the source of the active navigation signal, in nautical miles.
         * @param isInZoneOfConfusion Whether the source of the active navigation signal is a VOR and the airplane's position
         * is close enough to the VOR to render lateral deviation values unreliable.
         */
        trackSignal(distanceToSource, isInZoneOfConfusion) {
            const obsCourse = this.navObs.get();
            const isLoc = this.navHasLoc.get();
            const deviation = this.navSignal.get() > 0 ? this.navCdi.get() : null;
            if (isInZoneOfConfusion || deviation !== null) {
                const courseMag = isLoc ? this.navLocCourse.get() * Avionics.Utils.RAD2DEG : obsCourse;
                if (courseMag === null) {
                    return;
                }
                const deflection = SimVar.GetSimVarValue(this.deviationSimVar, exports.SimVarValueType.Number) / 127;
                const radialError = SimVar.GetSimVarValue(this.radialErrorSimVar, exports.SimVarValueType.Radians);
                const xtk = isInZoneOfConfusion ? 0 : distanceToSource * Math.sin(-radialError);
                const courseTrue = MagVar.magneticToTrue(courseMag, -this.navMagVar.get());
                const trueTrack = GNSSPublisher.getInstantaneousTrack();
                this.phase = this.phaseSelectorFunc(this.phase, deflection, xtk, courseTrue, trueTrack, isLoc, isInZoneOfConfusion);
                const phaseParams = this.phaseParameters[this.phase];
                const absInterceptAngle = phaseParams.lateralInterceptCurveFunc(distanceToSource, deflection, xtk, SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.Knots), isLoc);
                const interceptAngle = xtk > 0 ? -absInterceptAngle : absInterceptAngle;
                const courseToSteer = NavMath.normalizeHeading(courseTrue + interceptAngle);
                const trackError = (MathUtils.diffAngleDeg(trueTrack, courseToSteer) + 180) % 360 - 180;
                const maxBankAngle = phaseParams.maxBankAngleFunc();
                const desiredBank = MathUtils.clamp(phaseParams.desiredBankFunc(trackError), -maxBankAngle, maxBankAngle);
                if (isFinite(desiredBank)) {
                    phaseParams.driveBankFunc(desiredBank);
                }
            }
        }
        /**
         * Gets the lateral distance from PPOS to the nav signal.
         * @returns The distance value in nautical miles.
         */
        getNavDistance() {
            const navLla = this.navLla.get();
            if (navLla !== null) {
                return UnitType.GA_RADIAN.convertTo(GeoPoint.distance(navLla.lat, navLla.long, SimVar.GetSimVarValue('PLANE LATITUDE', exports.SimVarValueType.Degree), SimVar.GetSimVarValue('PLANE LONGITUDE', exports.SimVarValueType.Degree)), UnitType.NMILE);
            }
            else {
                return 5;
            }
        }
        /**
         * Checks whether the airplane is in the zone of confusion.
         * @param distanceToSource The distance from the airplane to the tuned station, in nautical miles.
         * @returns Whether the airplane is in the zone of confusion.
         */
        isInZoneOfConfusion(distanceToSource) {
            return distanceToSource < 2;
        }
        /**
         * Selects a phase to use while this director is active and tracking a navigation signal using default logic.
         *
         * Phase selection is based on whether lateral deflection, cross-track error, and course error exceed certain
         * thresholds. Initial phase selection after director activation defaults to intercept phase if any of the values
         * exceed their thresholds and to tracking phase otherwise. Phase switching from intercept to tracking occurs when
         * all values do not exceed their thresholds for a certain consecutive amount of time. Phase switching from tracking
         * to intercept occurs when at least one value exceeds its threshold for a certain consecutive amount of time.
         * @param currentPhase The current phase, or `undefined` if no phase has been selected since the last time the
         * director was activated.
         * @param deflection The lateral deflection of the desired track relative to the plane, normalized from `-1` to `1`.
         * Positive values indicate that the desired track is to the right of the plane.
         * @param xtk The cross-track error of the plane from the desired track, in nautical miles. Positive values indicate
         * that the plane is to the right of the track.
         * @param course The true course of the desired track, in degrees.
         * @param track The actual true ground track of the airplane, in degrees.
         * @param isLoc Whether the source of the tracked navigation signal is a localizer.
         * @param isInZoneOfConfusion Whether the source of the tracked navigation signal is a VOR and the airplane's
         * position is close enough to the VOR to render lateral deflection values unreliable.
         * @returns The phase to use while tracking the navigation signal.
         */
        defaultSelectPhase(currentPhase, deflection, xtk, course, track, isLoc, isInZoneOfConfusion) {
            const time = Date.now();
            const dt = this.lastPhaseUpdateTime === undefined ? 0 : MathUtils.clamp(time - this.lastPhaseUpdateTime, 0, 1000) * this.apValues.simRate.get();
            this.lastPhaseUpdateTime = time;
            if (currentPhase === exports.APNavDirectorPhase.Tracking) {
                if ((!isInZoneOfConfusion && Math.abs(deflection) >= this.phaseOptions.interceptDeflection)
                    || Math.abs(xtk) >= this.phaseOptions.interceptXtk
                    || MathUtils.diffAngleDeg(track, course, false) >= this.phaseOptions.interceptCourseError) {
                    if (this.phaseTransitionTimer <= 0) {
                        this.phaseTransitionTimer = this.phaseOptions.trackingSwitchDelay;
                        return exports.APNavDirectorPhase.Intercept;
                    }
                    else {
                        this.phaseTransitionTimer -= dt;
                    }
                }
                else {
                    this.phaseTransitionTimer = this.phaseOptions.interceptSwitchDelay;
                }
                return exports.APNavDirectorPhase.Tracking;
            }
            else {
                if (currentPhase === undefined) {
                    this.phaseTransitionTimer = 0;
                }
                if ((!isInZoneOfConfusion && Math.abs(deflection) <= this.phaseOptions.trackingDeflection)
                    && Math.abs(xtk) <= this.phaseOptions.trackingXtk
                    && MathUtils.diffAngleDeg(track, course, false) <= this.phaseOptions.trackingCourseError) {
                    if (this.phaseTransitionTimer <= 0) {
                        this.phaseTransitionTimer = this.phaseOptions.interceptSwitchDelay;
                        return exports.APNavDirectorPhase.Tracking;
                    }
                    else {
                        this.phaseTransitionTimer -= dt;
                    }
                }
                else {
                    this.phaseTransitionTimer = this.phaseOptions.trackingSwitchDelay;
                }
                return exports.APNavDirectorPhase.Intercept;
            }
        }
        /**
         * A default function that checks whether the director can be armed.
         * @param mode The director's lateral mode.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be armed.
         */
        static defaultCanArm(mode, apValues, navData) {
            const typeIsCorrect = navData.navSource.type === exports.NavSourceType.Nav && navData.navSource.index !== 0;
            if (typeIsCorrect) {
                if (mode === exports.APLateralModes.LOC) {
                    return navData.hasLoc;
                }
                else if (mode === exports.APLateralModes.VOR) {
                    return !navData.hasLoc;
                }
            }
            else {
                if (apValues.navToNavArmableLateralMode && apValues.navToNavArmableLateralMode() === mode) {
                    return true;
                }
            }
            return false;
        }
        /**
         * A default function that checks whether the director can be activated from an armed state.
         * @param mode The director's lateral mode.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be activated from an armed state.
         */
        static defaultCanActivate(mode, apValues, navData) {
            if (navData.navSource.type === exports.NavSourceType.Nav
                && navData.navSource.index !== 0
                && navData.deviation !== null
                && Math.abs(navData.deviation) < 1
                && (navData.hasLoc || navData.obsCourse !== null)) {
                const dtk = navData.hasLoc
                    ? navData.locCourse
                    : navData.obsCourse;
                if (dtk === null) {
                    return false;
                }
                const headingDiff = NavMath.diffAngle(SimVar.GetSimVarValue('PLANE HEADING DEGREES MAGNETIC', exports.SimVarValueType.Degree), dtk);
                const sensitivity = navData.hasLoc ? 1 : .6;
                if (Math.abs(navData.deviation * sensitivity) < 1 && Math.abs(headingDiff) < 110) {
                    return true;
                }
            }
            return false;
        }
        /**
         * A default function that checks whether the director can remain in the active state.
         * @param mode The director's lateral mode.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @param isInZoneOfConfusion Whether the source of the radio navigation data is a VOR and the airplane's position
         * is close enough to the VOR to render lateral deviation values unreliable.
         * @returns Whether the director can remain in the active state.
         */
        static defaultCanRemainActive(mode, apValues, navData, isInZoneOfConfusion) {
            if (navData.navSource.type !== exports.NavSourceType.Nav || navData.navSource.index === 0) {
                return false;
            }
            if (mode === exports.APLateralModes.LOC) {
                return navData.hasLoc
                    && navData.locCourse !== null
                    && navData.deviation !== null
                    && Math.abs(navData.deviation) < 1;
            }
            else if (mode === exports.APLateralModes.VOR) {
                return !navData.hasLoc
                    && navData.obsCourse !== null
                    && (isInZoneOfConfusion || (navData.deviation !== null && Math.abs(navData.deviation) < 1));
            }
            return false;
        }
        /**
         * Calculates an intercept angle, in degrees, to capture the desired track from a navigation signal using default
         * logic tuned for slow GA airplanes.
         * @param distanceToSource The distance from the plane to the source of the navigation signal, in nautical miles.
         * @param deflection The lateral deflection of the desired track relative to the plane, normalized from `-1` to `1`.
         * Positive values indicate that the desired track is to the right of the plane.
         * @param xtk The cross-track error of the plane from the desired track, in nautical miles. Positive values indicate
         * that the plane is to the right of the track.
         * @returns The intercept angle, in degrees, to capture the desired track from the navigation signal.
         */
        static defaultLateralInterceptCurve(distanceToSource, deflection, xtk) {
            let absInterceptAngle = Math.min(Math.pow(Math.abs(xtk) * 20, 1.35) + (Math.abs(xtk) * 50), 45);
            if (absInterceptAngle <= 2.5) {
                absInterceptAngle = NavMath.clamp(Math.abs(xtk * 150), 0, 2.5);
            }
            return absInterceptAngle;
        }
    }

    /**
     * A director that removes all vertical guidance from the autopilot system.
     */
    class APNoneVerticalDirector {
        constructor() {
            /** @inheritdoc */
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritdoc */
        activate() {
            this.state = exports.DirectorState.Active;
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.Alt, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.AltArm, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.FLC, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.VS, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.Pitch, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.GS, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.GSArm, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.TOGAPitch, 0);
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
        }
        /** @inheritdoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /** @inheritdoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritdoc */
        update() {
            /** No-op */
        }
    }
    /**
     * A director that removes all lateral guidance from the autopilot system.
     */
    class APNoneLateralDirector {
        constructor() {
            /** @inheritdoc */
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritdoc */
        activate() {
            this.state = exports.DirectorState.Active;
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.Bank, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.Heading, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.Nav, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.NavArm, 0);
            Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.WingLevel, 0);
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
        }
        /** @inheritdoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /** @inheritdoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritdoc */
        update() {
            /** No-op */
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * An autopilot pitch director.
     */
    class APPitchDirector {
        /**
         * Creates an instance of the LateralDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues are the AP Values subjects.
         * @param arg0 Options, or for the legacy ctor is the pitch increment, in degrees,
         * to use when the user presses the pitch inc/dec keys (default: 0.5).
         * @param minPitch For the legacy ctor, is the negative minimum pitch angle, in degrees, to clamp the pitch to. (default: -15).
         * @param maxPitch For the legacy ctor, is the positive maximum pitch angle, in degrees, to clamp the pitch to. (default: 20).
         */
        constructor(bus, apValues, arg0, minPitch, maxPitch) {
            this.apValues = apValues;
            this.selectedPitch = 0;
            // handle legacy constructor args
            const options = typeof arg0 === 'number' ? {
                pitchIncrement: arg0,
                minPitch,
                maxPitch,
            }
                : arg0;
            // set options
            this.pitchIncrement = APPitchDirector.getOptionFunction(0.5, options === null || options === void 0 ? void 0 : options.pitchIncrement);
            this.minPitch = APPitchDirector.getOptionFunction(-15, options === null || options === void 0 ? void 0 : options.minPitch);
            this.maxPitch = APPitchDirector.getOptionFunction(20, options === null || options === void 0 ? void 0 : options.maxPitch);
            this.quantisePitch = !!(options === null || options === void 0 ? void 0 : options.quantisePitch);
            this.state = exports.DirectorState.Inactive;
            this.apValues.selectedPitch.sub((p) => {
                this.selectedPitch = p;
                if (this.state == exports.DirectorState.Active) {
                    // send it in again to make sure its clamped
                    this.setPitch && this.setPitch(p);
                }
            });
            // setup inc/dec event intercept
            KeyEventManager.getManager(bus).then(manager => {
                this.keyEventManager = manager;
                manager.interceptKey('AP_PITCH_REF_INC_UP', false);
                manager.interceptKey('AP_PITCH_REF_INC_DN', false);
                const keySub = bus.getSubscriber();
                keySub.on('key_intercept').handle(this.onKeyIntercepted.bind(this));
            });
        }
        /**
         * Get a function that returns the value for an option property.
         * @param defaultValue The default value.
         * @param optValue The {@link APPitchDirectorOptions} value.
         * @returns Either the function given in the options, a function returning the value given in the options,
         * or a function returning the default value.
         */
        static getOptionFunction(defaultValue, optValue) {
            switch (typeof optValue) {
                case 'number':
                    return () => optValue;
                case 'function':
                    return optValue;
                default:
                    return () => defaultValue;
            }
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            const currentPitch = SimVar.GetSimVarValue('PLANE PITCH DEGREES', exports.SimVarValueType.Degree);
            this.setPitch && this.setPitch(this.getTargetPitch(currentPitch));
            SimVar.SetSimVarValue('AUTOPILOT PITCH HOLD', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT PITCH HOLD', 'Bool', false);
        }
        /**
         * Responds to key intercepted events.
         * @param k the key event data
         */
        onKeyIntercepted(k) {
            switch (k.key) {
                case 'AP_PITCH_REF_INC_UP':
                case 'AP_PITCH_REF_INC_DN':
                    this.setPitch && this.setPitch(this.getTargetPitch(this.selectedPitch + (k.key === 'AP_PITCH_REF_INC_UP' ? -this.pitchIncrement() : this.pitchIncrement())));
                    break;
                default:
                    return;
            }
        }
        /**
         * Updates this director.
         */
        update() {
            //noop
        }
        /**
         * Get the desired pitch, clamped between min and max, and quantised to pitch increment if enabled.
         * @param desiredPitch The desired pitch in degrees (-ve is up).
         * @returns the target pitch in degrees (-ve is up).
         */
        getTargetPitch(desiredPitch) {
            return MathUtils.clamp(this.quantisePitch ? MathUtils.round(desiredPitch, this.pitchIncrement()) : desiredPitch, -this.maxPitch(), -this.minPitch());
        }
    }

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A pitch level autopilot director.
     */
    class APPitchLvlDirector {
        /**
         * Creates an instance of the APPitchLvlDirector.
         * @param apValues are the ap selected values for the autopilot.
         */
        constructor(apValues) {
            this.apValues = apValues;
            this.state = exports.DirectorState.Inactive;
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.drivePitch && this.drivePitch(0, true, true);
            }
        }
    }

    /**
     * An autopilot roll director.
     */
    class APRollDirector {
        /**
         * Creates an instance of the LateralDirector.
         * @param apValues The AP Values.
         * @param options Options to configure the new director.
         */
        constructor(apValues, options) {
            var _a;
            this.apValues = apValues;
            this.currentBankRef = 0;
            this.desiredBank = 0;
            this.actualBank = 0;
            const minBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.minBankAngle) !== null && _a !== void 0 ? _a : 0;
            if (typeof minBankAngleOpt === 'number') {
                this.minBankAngleFunc = () => minBankAngleOpt;
            }
            else {
                this.minBankAngleFunc = minBankAngleOpt;
            }
            const maxBankAngleOpt = options === null || options === void 0 ? void 0 : options.maxBankAngle;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.state = exports.DirectorState.Inactive;
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            this.actualBank = SimVar.GetSimVarValue('PLANE BANK DEGREES', exports.SimVarValueType.Degree);
            this.currentBankRef = this.actualBank;
            const maxBank = this.maxBankAngleFunc();
            const minBank = this.minBankAngleFunc();
            if (Math.abs(this.currentBankRef) < minBank) {
                this.desiredBank = 0;
            }
            else {
                this.desiredBank = MathUtils.clamp(this.currentBankRef, -maxBank, maxBank);
            }
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT BANK HOLD', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.desiredBank = 0;
            SimVar.SetSimVarValue('AUTOPILOT BANK HOLD', 'Bool', false);
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.driveBankFunc(this.desiredBank);
            }
        }
    }

    /**
     * An autopilot roll-steering director. This director uses roll-steering commands to drive flight director bank
     * commands.
     */
    class APRollSteerDirector {
        /**
         * Creates a new instance of APRollSteerDirector.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param steerCommand The steering command used by this director.
         * @param options Options to configure the new director.
         */
        constructor(apValues, steerCommand, options) {
            var _a, _b, _c, _d, _e;
            this.apValues = apValues;
            this.steerCommand = steerCommand;
            this.callbackState = {
                steerCommand: this.steerCommand.get()
            };
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.onArmFunc = options === null || options === void 0 ? void 0 : options.onArm;
            this.onActivateFunc = options === null || options === void 0 ? void 0 : options.onActivate;
            this.onDeactivateFunc = options === null || options === void 0 ? void 0 : options.onDeactivate;
            this.canArmFunc = (_b = options === null || options === void 0 ? void 0 : options.canArm) !== null && _b !== void 0 ? _b : ((apValuesInner, state) => state.steerCommand.isValid);
            this.canRemainArmedFunc = (_c = options === null || options === void 0 ? void 0 : options.canRemainArmed) !== null && _c !== void 0 ? _c : ((apValuesInner, state) => state.steerCommand.isValid);
            this.canActivateFunc = (_d = options === null || options === void 0 ? void 0 : options.canActivate) !== null && _d !== void 0 ? _d : ((apValuesInner, state) => state.steerCommand.isValid);
            this.canRemainActiveFunc = (_e = options === null || options === void 0 ? void 0 : options.canRemainActive) !== null && _e !== void 0 ? _e : ((apValuesInner, state) => state.steerCommand.isValid);
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritDoc */
        activate() {
            this.state = exports.DirectorState.Active;
            this.updateCallbackState();
            this.onActivateFunc && this.onActivateFunc(this.apValues, this.callbackState);
            this.onActivate && this.onActivate();
        }
        /** @inheritDoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.updateCallbackState();
                if (this.canArmFunc(this.apValues, this.callbackState)) {
                    this.state = exports.DirectorState.Armed;
                    this.onArmFunc && this.onArmFunc(this.apValues, this.callbackState);
                    this.onArm && this.onArm();
                }
            }
        }
        /** @inheritDoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.updateCallbackState();
            this.onDeactivateFunc && this.onDeactivateFunc(this.apValues, this.callbackState);
            this.onDeactivate && this.onDeactivate();
        }
        /** @inheritDoc */
        update() {
            this.updateCallbackState();
            if (this.state === exports.DirectorState.Armed) {
                if (!this.canRemainArmedFunc(this.apValues, this.callbackState)) {
                    this.deactivate();
                    return;
                }
                else {
                    this.tryActivate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                if (!this.canRemainActiveFunc(this.apValues, this.callbackState)) {
                    this.deactivate();
                    return;
                }
                else {
                    const maxBankAngle = this.maxBankAngleFunc();
                    const desiredBankAngle = MathUtils.clamp(this.steerCommand.get().desiredBankAngle, -maxBankAngle, maxBankAngle);
                    if (isFinite(desiredBankAngle)) {
                        this.driveBankFunc(desiredBankAngle);
                    }
                }
            }
        }
        /**
         * Updates this director's callback state.
         */
        updateCallbackState() {
            this.callbackState.steerCommand = this.steerCommand.get();
        }
        /**
         * Attempts to activate this director from an armed state.
         */
        tryActivate() {
            if (this.canActivateFunc(this.apValues, this.callbackState)) {
                this.activate();
            }
        }
    }

    /**
     * A heading autopilot director.
     */
    class APTrkDirector {
        /**
         * Creates a new instance of APHdgDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director.
         */
        constructor(bus, apValues, options) {
            var _a, _b, _c;
            this.bus = bus;
            this.apValues = apValues;
            this.toGaTrack = 0;
            this.magVar = ConsumerValue.create(null, 0);
            this.lastTrackDiff = undefined;
            this.lockedTurnDirection = undefined;
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.turnReversalThreshold = (_b = options === null || options === void 0 ? void 0 : options.turnReversalThreshold) !== null && _b !== void 0 ? _b : 0;
            this.isToGaMode = (_c = options === null || options === void 0 ? void 0 : options.isToGaMode) !== null && _c !== void 0 ? _c : false;
            this.state = exports.DirectorState.Inactive;
            this.magVar.setConsumer(this.bus.getSubscriber().on('magvar'));
            this.pauseSubs();
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.magVar.resume();
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.magVar.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            if (!this.isToGaMode) {
                SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', true);
            }
            else {
                this.toGaTrack = this.getMagneticTrack();
            }
            this.state = exports.DirectorState.Active;
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        async deactivate() {
            if (!this.isToGaMode) {
                await SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', false);
            }
            this.state = exports.DirectorState.Inactive;
            this.pauseSubs();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                let bank = 0;
                if (this.isToGaMode) {
                    if (Simplane.getIsGrounded()) {
                        this.toGaTrack = this.getMagneticTrack();
                    }
                    else {
                        bank = this.desiredBank(this.toGaTrack);
                    }
                }
                else {
                    bank = this.desiredBank(this.apValues.selectedHeading.get());
                }
                this.driveBankFunc(bank);
            }
        }
        /**
         * Gets a desired bank from a Target Selected Track.
         * @param targetTrack The target track.
         * @returns The desired bank angle.
         */
        desiredBank(targetTrack) {
            const currentTrack = this.getMagneticTrack();
            const trackDiff = MathUtils.diffAngleDeg(currentTrack, targetTrack);
            let turnDirection = undefined;
            let directionalTrackDiff;
            if (this.lockedTurnDirection !== undefined) {
                turnDirection = this.lockedTurnDirection;
                directionalTrackDiff = turnDirection === 'left' ? (360 - trackDiff) % 360 : trackDiff;
                if (directionalTrackDiff >= this.turnReversalThreshold) {
                    turnDirection = undefined;
                }
                else if (this.lastTrackDiff !== undefined) {
                    // Check if the track difference passed through zero in the positive to negative direction since the last
                    // update. If so, we may need to issue a turn reversal.
                    const trackDiffDelta = (MathUtils.diffAngleDeg(this.lastTrackDiff, directionalTrackDiff) + 180) % 360 - 180; // -180 to +180
                    if (this.lastTrackDiff + trackDiffDelta < 0) {
                        turnDirection = undefined;
                    }
                }
            }
            if (turnDirection === undefined) {
                turnDirection = NavMath.getTurnDirection(currentTrack, targetTrack);
                directionalTrackDiff = turnDirection === 'left' ? (360 - trackDiff) % 360 : trackDiff;
            }
            if (this.turnReversalThreshold > 180) {
                this.lockedTurnDirection = turnDirection;
            }
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            this.lastTrackDiff = directionalTrackDiff;
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            let baseBank = Math.min(1.25 * directionalTrackDiff, this.maxBankAngleFunc());
            baseBank *= (turnDirection === 'left' ? 1 : -1);
            return baseBank;
        }
        /**
         * Gets the instantanious magnetic track.
         * @returns Magnetic Track, in degrees.
         */
        getMagneticTrack() {
            const trueTrack = GNSSPublisher.getInstantaneousTrack();
            return NavMath.normalizeHeading(trueTrack - this.magVar.get());
        }
    }

    /**
     * An autopilot track hold director.
     * Levels the wings upon activation, and then holds the captured track
     */
    class APTrkHoldDirector {
        /**
         * Creates an instance of the track hold director.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director. Option values default to the following if not defined:
         * * `maxBankAngle`: `undefined`
         * * `isToGaMode`: `false`
         */
        constructor(bus, apValues, options) {
            var _a;
            this.bus = bus;
            this.apValues = apValues;
            this.magVar = ConsumerValue.create(null, 0);
            /** track captured at wings level, or null if not yet captured */
            this.capturedTrack = null;
            const maxBankAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _a !== void 0 ? _a : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default:
                    this.maxBankAngleFunc = this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            }
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.state = exports.DirectorState.Inactive;
            this.magVar.setConsumer(this.bus.getSubscriber().on('magvar'));
            this.pauseSubs();
        }
        /** Resumes Subscriptions. */
        resumeSubs() {
            this.magVar.resume();
        }
        /** Pauses Subscriptions. */
        pauseSubs() {
            this.magVar.pause();
        }
        /**
         * Activates this director.
         */
        activate() {
            this.resumeSubs();
            this.state = exports.DirectorState.Active;
            this.capturedTrack = null;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', true);
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT HEADING LOCK', 'Bool', false);
            this.pauseSubs();
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                if (this.capturedTrack === null) {
                    const currentBank = SimVar.GetSimVarValue('PLANE BANK DEGREES', exports.SimVarValueType.Degree);
                    if (Math.abs(currentBank) < APTrkHoldDirector.MIN_BANK_THRESHOLD) {
                        this.capturedTrack = this.getMagneticTrack();
                    }
                }
                this.driveBankFunc(this.capturedTrack !== null ? this.desiredBank(this.capturedTrack) : 0);
            }
        }
        /**
         * Gets a desired bank from a Target Selected Track.
         * @param targetTrack The target track.
         * @returns The desired bank angle.
         */
        desiredBank(targetTrack) {
            const magneticTrack = this.getMagneticTrack();
            const turnDirection = NavMath.getTurnDirection(magneticTrack, targetTrack);
            const trackDiff = Math.abs(NavMath.diffAngle(magneticTrack, targetTrack));
            let baseBank = Math.min(1.25 * trackDiff, this.maxBankAngleFunc());
            baseBank *= (turnDirection === 'left' ? 1 : -1);
            return baseBank;
        }
        /**
         * Gets the instantanious magnetic track.
         * @returns Magnetic Track, in degrees.
         */
        getMagneticTrack() {
            const trueTrack = GNSSPublisher.getInstantaneousTrack();
            return NavMath.normalizeHeading(trueTrack - this.magVar.get());
        }
    }
    /** bank angle below which we capture the track */
    APTrkHoldDirector.MIN_BANK_THRESHOLD = 1;

    /// <reference types="@microsoft/msfs-types/js/simvar" />
    /**
     * A vertical speed autopilot director.
     */
    class APVSDirector {
        /**
         * Creates an instance of the LateralDirector.
         * @param apValues are the ap selected values for the autopilot.
         * @param vsIncrement The number that vertical speed can be incremented by, in feet per minute.
         * Upon activation, the actual vs will be rounded using this increment.
         * If undefined, the value will not be rounded before passed to the sim. Defaults to undefined.
         */
        constructor(apValues, vsIncrement = undefined) {
            this.apValues = apValues;
            this.vsIncrement = vsIncrement;
            this.state = exports.DirectorState.Inactive;
            //PM Modif: set pitchServoRate for this director
            this.pitchServoRate = AutopilotDriver.PITCH_SERVO_RATE;
            //End PM Modif
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            const currentVs = this.vsIncrement === undefined
                ? Simplane.getVerticalSpeed()
                : MathUtils.round(Simplane.getVerticalSpeed(), this.vsIncrement);
            Coherent.call('AP_VS_VAR_SET_ENGLISH', 1, currentVs);
            SimVar.SetSimVarValue('AUTOPILOT VERTICAL HOLD', 'Bool', true);
            //PM Modif: set temporarily a high level rate for the pitch servo to avoid any bump when engaging VS
            this.pitchServoRate = 100;
            setTimeout(() => {
                this.pitchServoRate = AutopilotDriver.PITCH_SERVO_RATE;
            }, 500);
            //End PM Modif
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            SimVar.SetSimVarValue('AUTOPILOT VERTICAL HOLD', 'Bool', false);
        }
        /**
         * Updates this director.
         */
        update() {
            //PM Modif: use our own value for pitch servo rate
            if (this.state === exports.DirectorState.Active) {
                this.drivePitch && this.drivePitch(this.getDesiredPitch(), true, true, this.pitchServoRate);
            }
            //End PM Modif
        }
        /**
         * Gets a desired pitch from the selected vs value.
         * @returns The desired pitch angle.
         */
        getDesiredPitch() {
            const tas = SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.Knots);
            const desiredPitch = this.getFpa(UnitType.NMILE.convertTo(tas / 60, UnitType.FOOT), this.apValues.selectedVerticalSpeed.get());
            return -MathUtils.clamp(isNaN(desiredPitch) ? 0 : desiredPitch, -15, 15);
        }
        /**
         * Gets a desired fpa.
         * @param distance is the distance traveled per minute.
         * @param altitude is the vertical speed per minute.
         * @returns The desired pitch angle.
         */
        getFpa(distance, altitude) {
            return UnitType.RADIAN.convertTo(Math.atan(altitude / distance), UnitType.DEGREE);
        }
    }

    /**
     * Phases used by {@link APBackCourseDirector} when active and tracking a localizer signal.
     */
    exports.APBackCourseDirectorPhase = void 0;
    (function (APBackCourseDirectorPhase) {
        APBackCourseDirectorPhase["Intercept"] = "Intercept";
        APBackCourseDirectorPhase["Tracking"] = "Tracking";
    })(exports.APBackCourseDirectorPhase || (exports.APBackCourseDirectorPhase = {}));
    /**
     * An autopilot director that provides lateral guidance by tracking a back-course signal from a localizer radio
     * navigation aid.
     *
     * Requires that the navigation radio topics defined in {@link NavComEvents} be published to the event bus in order to
     * function properly.
     */
    class APBackCourseDirector {
        /**
         * Creates a new instance of APBackCourseDirector.
         * @param bus The event bus to use with this instance.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options with which to configure the director.
         */
        constructor(bus, apValues, options) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
            var _z, _0, _1, _2, _3, _4, _5, _6;
            this.bus = bus;
            this.apValues = apValues;
            this.navSource = {
                index: 0,
                type: exports.NavSourceType.Nav,
            };
            this.deviationSimVar = 'NAV CDI:1';
            this.radialErrorSimVar = 'NAV RADIAL ERROR:1';
            this.navFrequency = ConsumerValue.create(null, 0);
            this.navSignal = ConsumerValue.create(null, 0);
            this.navHasNav = ConsumerValue.create(null, false);
            this.navCdi = ConsumerValue.create(null, null);
            this.navHasLoc = ConsumerValue.create(null, false);
            this.navLocCourse = ConsumerValue.create(null, 0);
            this.navMagVar = ConsumerValue.create(null, 0);
            this.navLla = ConsumerValue.create(null, null);
            this.navData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0,
                signal: 0,
                hasNav: false,
                hasLoc: false,
                locCourse: null,
                deviation: null
            };
            this.activateNavData = {
                navSource: { index: 0, type: exports.NavSourceType.Nav },
                frequency: 0,
            };
            this.phase = undefined;
            this.lastPhaseUpdateTime = undefined;
            this.phaseTransitionTimer = 0;
            this.state = exports.DirectorState.Inactive;
            const interceptParams = {};
            const trackingParams = {};
            const maxBankAngleFunc = (_a = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngle)) !== null && _a !== void 0 ? _a : this.apValues.maxBankAngle.get.bind(this.apValues.maxBankAngle);
            interceptParams.maxBankAngleFunc = (_b = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngleIntercept)) !== null && _b !== void 0 ? _b : maxBankAngleFunc;
            trackingParams.maxBankAngleFunc = (_c = this.resolveMaxBankAngleOption(options === null || options === void 0 ? void 0 : options.maxBankAngleTracking)) !== null && _c !== void 0 ? _c : maxBankAngleFunc;
            const driveBankFunc = (_d = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRate)) !== null && _d !== void 0 ? _d : ((bank) => {
                if (isFinite(bank) && this.driveBank) {
                    this.driveBank(bank);
                }
            });
            interceptParams.driveBankFunc = (_e = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRateIntercept)) !== null && _e !== void 0 ? _e : driveBankFunc;
            trackingParams.driveBankFunc = (_f = this.resolveBankRateOption(options === null || options === void 0 ? void 0 : options.bankRateTracking)) !== null && _f !== void 0 ? _f : driveBankFunc;
            this.canArm = (_g = options === null || options === void 0 ? void 0 : options.canArm) !== null && _g !== void 0 ? _g : APBackCourseDirector.defaultCanArm;
            this.canActivate = (_h = options === null || options === void 0 ? void 0 : options.canActivate) !== null && _h !== void 0 ? _h : APBackCourseDirector.defaultCanActivate;
            this.canRemainActive = (_j = options === null || options === void 0 ? void 0 : options.canRemainActive) !== null && _j !== void 0 ? _j : APBackCourseDirector.defaultCanRemainActive;
            this.phaseOptions = Object.assign({}, options === null || options === void 0 ? void 0 : options.phaseOptions);
            (_k = (_z = this.phaseOptions).interceptDeflection) !== null && _k !== void 0 ? _k : (_z.interceptDeflection = 0.25);
            (_l = (_0 = this.phaseOptions).interceptXtk) !== null && _l !== void 0 ? _l : (_0.interceptXtk = Infinity);
            (_m = (_1 = this.phaseOptions).interceptCourseError) !== null && _m !== void 0 ? _m : (_1.interceptCourseError = 5);
            (_o = (_2 = this.phaseOptions).interceptSwitchDelay) !== null && _o !== void 0 ? _o : (_2.interceptSwitchDelay = 5000);
            (_p = (_3 = this.phaseOptions).trackingDeflection) !== null && _p !== void 0 ? _p : (_3.trackingDeflection = 0.125);
            (_q = (_4 = this.phaseOptions).trackingXtk) !== null && _q !== void 0 ? _q : (_4.trackingXtk = Infinity);
            (_r = (_5 = this.phaseOptions).trackingCourseError) !== null && _r !== void 0 ? _r : (_5.trackingCourseError = 2);
            (_s = (_6 = this.phaseOptions).trackingSwitchDelay) !== null && _s !== void 0 ? _s : (_6.trackingSwitchDelay = 5000);
            this.phaseSelectorFunc = (_t = options === null || options === void 0 ? void 0 : options.phaseSelector) !== null && _t !== void 0 ? _t : this.defaultSelectPhase.bind(this);
            const lateralInterceptCurveFunc = (_u = options === null || options === void 0 ? void 0 : options.lateralInterceptCurve) !== null && _u !== void 0 ? _u : APBackCourseDirector.defaultLateralInterceptCurve;
            interceptParams.lateralInterceptCurveFunc = (_v = options === null || options === void 0 ? void 0 : options.lateralInterceptCurveIntercept) !== null && _v !== void 0 ? _v : lateralInterceptCurveFunc;
            trackingParams.lateralInterceptCurveFunc = (_w = options === null || options === void 0 ? void 0 : options.lateralInterceptCurveTracking) !== null && _w !== void 0 ? _w : lateralInterceptCurveFunc;
            interceptParams.desiredBankFunc = (_x = options === null || options === void 0 ? void 0 : options.desiredBankIntercept) !== null && _x !== void 0 ? _x : ((trackError) => -trackError * 2);
            trackingParams.desiredBankFunc = (_y = options === null || options === void 0 ? void 0 : options.desiredBankTracking) !== null && _y !== void 0 ? _y : ((trackError) => -trackError * 1.25);
            this.phaseParameters = {
                [exports.APBackCourseDirectorPhase.Intercept]: interceptParams,
                [exports.APBackCourseDirectorPhase.Tracking]: trackingParams
            };
            this.forceNavSource = options === null || options === void 0 ? void 0 : options.forceNavSource;
            this.initCdiSourceSubs();
        }
        /**
         * Resolves a maximum bank angle option to a function which returns a maximum bank angle.
         * @param opt The option to resolve.
         * @returns A function which returns a maximum bank angle as defined by the specified option, or `undefined` if the
         * option is not defined.
         */
        resolveMaxBankAngleOption(opt) {
            switch (typeof opt) {
                case 'number':
                    return () => opt;
                case 'function':
                    return opt;
                default:
                    return undefined;
            }
        }
        /**
         * Resolves a bank rate option to a function which drives the commanded bank angle.
         * @param opt The option to resolve.
         * @returns A function which drives the commanded bank angle in accordance with the specified bank rate option, or
         * `undefined` if the option is not defined.
         */
        resolveBankRateOption(opt) {
            switch (typeof opt) {
                case 'number':
                    return bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, opt * this.apValues.simRate.get());
                        }
                    };
                case 'function':
                    return bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, opt() * this.apValues.simRate.get());
                        }
                    };
                default:
                    return undefined;
            }
        }
        /**
         * Initializes this director's subscription to the autopilot's CDI source. If this director is forced to use a
         * specific CDI source, then the autopilot's CDI source will be ignored.
         */
        initCdiSourceSubs() {
            if (this.forceNavSource !== undefined) {
                this.onCdiSourceChanged({
                    index: this.forceNavSource,
                    type: exports.NavSourceType.Nav,
                });
            }
            else {
                this.navSource = {
                    index: 0,
                    type: exports.NavSourceType.Nav,
                };
                this.apValues.cdiSource.sub(this.onCdiSourceChanged.bind(this), true);
            }
        }
        /**
         * Responds to when the CDI source used by this director changes.
         * @param source The new CDI source to use.
         */
        onCdiSourceChanged(source) {
            Object.assign(this.navSource, source);
            if (source.type === exports.NavSourceType.Nav && source.index >= 1 && source.index <= 4) {
                const index = source.index;
                this.deviationSimVar = `NAV CDI:${index}`;
                this.radialErrorSimVar = `NAV RADIAL ERROR:${index}`;
                const sub = this.bus.getSubscriber();
                this.navFrequency.setConsumerWithDefault(sub.on(`nav_active_frequency_${index}`), 0);
                this.navSignal.setConsumerWithDefault(sub.on(`nav_signal_${index}`), 0);
                this.navHasNav.setConsumerWithDefault(sub.on(`nav_has_nav_${index}`), false);
                this.navCdi.setConsumerWithDefault(sub.on(`nav_cdi_${index}`), null);
                this.navHasLoc.setConsumerWithDefault(sub.on(`nav_localizer_${index}`), false);
                this.navLocCourse.setConsumerWithDefault(sub.on(`nav_localizer_crs_${index}`), 0);
                this.navMagVar.setConsumerWithDefault(sub.on(`nav_magvar_${index}`), 0);
                this.navLla.setConsumerWithDefault(sub.on(`nav_lla_${index}`), null);
            }
            else {
                this.navFrequency.reset(0);
                this.navSignal.reset(0);
                this.navHasNav.reset(false);
                this.navCdi.reset(null);
                this.navHasLoc.reset(false);
                this.navLocCourse.reset(0);
                this.navMagVar.reset(0);
                this.navLla.reset(null);
            }
        }
        /**
         * Updates this director's radio navigation data.
         */
        updateNavData() {
            Object.assign(this.navData.navSource, this.navSource);
            this.navData.frequency = this.navFrequency.get();
            this.navData.signal = this.navSignal.get();
            this.navData.hasNav = this.navHasNav.get();
            this.navData.hasLoc = this.navHasLoc.get();
            this.navData.locCourse = this.navData.hasLoc ? this.navLocCourse.get() * Avionics.Utils.RAD2DEG : null;
            this.navData.deviation = this.navData.signal > 0 && this.navData.hasNav ? this.navCdi.get() : null;
            if (this.navData.deviation !== null) {
                this.navData.deviation /= 127;
            }
        }
        /**
         * Activates this director.
         */
        activate() {
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            SimVar.SetSimVarValue('AUTOPILOT NAV1 LOCK', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT BACKCOURSE HOLD', 'Bool', true);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
            this.state = exports.DirectorState.Active;
            Object.assign(this.activateNavData.navSource, this.navSource);
            this.activateNavData.frequency = this.navFrequency.get();
        }
        /**
         * Arms this director.
         */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.updateNavData();
                if (this.canArm(this.apValues, this.navData)) {
                    this.state = exports.DirectorState.Armed;
                    if (this.onArm !== undefined) {
                        this.onArm();
                    }
                    SimVar.SetSimVarValue('AUTOPILOT NAV1 LOCK', 'Bool', true);
                    SimVar.SetSimVarValue('AUTOPILOT BACKCOURSE HOLD', 'Bool', true);
                    SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', true);
                }
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.phase = undefined;
            SimVar.SetSimVarValue('AUTOPILOT NAV1 LOCK', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT BACKCOURSE HOLD', 'Bool', false);
            SimVar.SetSimVarValue('AUTOPILOT APPROACH ACTIVE', 'Bool', false);
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Armed) {
                this.updateNavData();
                if (!this.canArm(this.apValues, this.navData)) {
                    this.deactivate();
                }
                else if (this.canActivate(this.apValues, this.navData)) {
                    this.activate();
                }
            }
            if (this.state === exports.DirectorState.Active) {
                this.updateNavData();
                if (!this.canRemainActive(this.apValues, this.navData, this.activateNavData)) {
                    this.deactivate();
                }
                else {
                    this.trackSignal();
                }
            }
        }
        /**
         * Tracks the active localizer signal received by this director.
         */
        trackSignal() {
            const deviation = this.navSignal.get() > 0 && this.navHasNav.get() ? this.navCdi.get() : null;
            if (deviation !== null) {
                const courseMag = NavMath.normalizeHeading(this.navLocCourse.get() * Avionics.Utils.RAD2DEG + 180);
                const distanceToSource = this.getNavDistance();
                const deflection = -SimVar.GetSimVarValue(this.deviationSimVar, exports.SimVarValueType.Number) / 127;
                const radialError = SimVar.GetSimVarValue(this.radialErrorSimVar, exports.SimVarValueType.Radians);
                const xtk = distanceToSource * Math.sin(radialError);
                const courseTrue = MagVar.magneticToTrue(courseMag, -this.navMagVar.get());
                const trueTrack = GNSSPublisher.getInstantaneousTrack();
                this.phase = this.phaseSelectorFunc(this.phase, deflection, xtk, courseTrue, trueTrack);
                const phaseParams = this.phaseParameters[this.phase];
                const absInterceptAngle = phaseParams.lateralInterceptCurveFunc(distanceToSource, deflection, xtk, SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.Knots));
                const interceptAngle = xtk > 0 ? -absInterceptAngle : absInterceptAngle;
                const courseToSteer = NavMath.normalizeHeading(courseTrue + interceptAngle);
                const trackError = (MathUtils.diffAngleDeg(trueTrack, courseToSteer) + 180) % 360 - 180;
                const maxBankAngle = phaseParams.maxBankAngleFunc();
                const desiredBank = MathUtils.clamp(phaseParams.desiredBankFunc(trackError), -maxBankAngle, maxBankAngle);
                if (isFinite(desiredBank)) {
                    phaseParams.driveBankFunc(desiredBank);
                }
            }
        }
        /**
         * Gets the lateral distance from PPOS to the nav signal.
         * @returns The distance value in nautical miles.
         */
        getNavDistance() {
            const navLla = this.navLla.get();
            if (navLla !== null) {
                return UnitType.GA_RADIAN.convertTo(GeoPoint.distance(navLla.lat, navLla.long, SimVar.GetSimVarValue('PLANE LATITUDE', exports.SimVarValueType.Degree), SimVar.GetSimVarValue('PLANE LONGITUDE', exports.SimVarValueType.Degree)), UnitType.NMILE);
            }
            else {
                return 5;
            }
        }
        /**
         * Selects a phase to use while this director is active and tracking a navigation signal using default logic.
         *
         * Phase selection is based on whether lateral deflection, cross-track error, and course error exceed certain
         * thresholds. Initial phase selection after director activation defaults to intercept phase if any of the values
         * exceed their thresholds and to tracking phase otherwise. Phase switching from intercept to tracking occurs when
         * all values do not exceed their thresholds for a certain consecutive amount of time. Phase switching from tracking
         * to intercept occurs when at least one value exceeds its threshold for a certain consecutive amount of time.
         * @param currentPhase The current phase, or `undefined` if no phase has been selected since the last time the
         * director was activated.
         * @param deflection The lateral deflection of the desired track relative to the plane, normalized from `-1` to `1`.
         * Positive values indicate that the desired track is to the right of the plane.
         * @param xtk The cross-track error of the plane from the desired track, in nautical miles. Positive values indicate
         * that the plane is to the right of the track.
         * @param course The true course of the desired track, in degrees.
         * @param track The actual true ground track of the airplane, in degrees.
         * @returns The phase to use while tracking the navigation signal.
         */
        defaultSelectPhase(currentPhase, deflection, xtk, course, track) {
            const time = Date.now();
            const dt = this.lastPhaseUpdateTime === undefined ? 0 : MathUtils.clamp(time - this.lastPhaseUpdateTime, 0, 1000) * this.apValues.simRate.get();
            this.lastPhaseUpdateTime = time;
            if (currentPhase === exports.APBackCourseDirectorPhase.Tracking) {
                if (Math.abs(deflection) >= this.phaseOptions.interceptDeflection
                    || Math.abs(xtk) >= this.phaseOptions.interceptXtk
                    || MathUtils.diffAngleDeg(track, course, false) >= this.phaseOptions.interceptCourseError) {
                    if (this.phaseTransitionTimer <= 0) {
                        this.phaseTransitionTimer = this.phaseOptions.trackingSwitchDelay;
                        return exports.APBackCourseDirectorPhase.Intercept;
                    }
                    else {
                        this.phaseTransitionTimer -= dt;
                    }
                }
                else {
                    this.phaseTransitionTimer = this.phaseOptions.interceptSwitchDelay;
                }
                return exports.APBackCourseDirectorPhase.Tracking;
            }
            else {
                if (currentPhase === undefined) {
                    this.phaseTransitionTimer = 0;
                }
                if (Math.abs(deflection) <= this.phaseOptions.trackingDeflection
                    && Math.abs(xtk) <= this.phaseOptions.trackingXtk
                    && MathUtils.diffAngleDeg(track, course, false) <= this.phaseOptions.trackingCourseError) {
                    if (this.phaseTransitionTimer <= 0) {
                        this.phaseTransitionTimer = this.phaseOptions.interceptSwitchDelay;
                        return exports.APBackCourseDirectorPhase.Tracking;
                    }
                    else {
                        this.phaseTransitionTimer -= dt;
                    }
                }
                else {
                    this.phaseTransitionTimer = this.phaseOptions.trackingSwitchDelay;
                }
                return exports.APBackCourseDirectorPhase.Intercept;
            }
        }
        /**
         * A default function that checks whether the director can be armed.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be armed.
         */
        static defaultCanArm(apValues, navData) {
            return navData.navSource.type === exports.NavSourceType.Nav && navData.navSource.index !== 0 && navData.hasLoc;
        }
        /**
         * A default function that checks whether the director can be activated from an armed state.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can be activated from an armed state.
         */
        static defaultCanActivate(apValues, navData) {
            if (navData.navSource.type === exports.NavSourceType.Nav
                && navData.navSource.index !== 0
                && navData.hasLoc
                && navData.locCourse !== null
                && navData.deviation !== null
                && Math.abs(navData.deviation) < 1) {
                const dtk = NavMath.normalizeHeading(navData.locCourse + 180);
                const headingDiff = NavMath.diffAngle(SimVar.GetSimVarValue('PLANE HEADING DEGREES MAGNETIC', exports.SimVarValueType.Degree), dtk);
                if (Math.abs(headingDiff) < 110) {
                    return true;
                }
            }
            return false;
        }
        /**
         * A default function that checks whether the director can remain in the active state.
         * @param apValues Autopilot values from the director's parent autopilot.
         * @param navData The current radio navigation data received by the director.
         * @returns Whether the director can remain in the active state.
         */
        static defaultCanRemainActive(apValues, navData) {
            return navData.navSource.type === exports.NavSourceType.Nav
                && navData.navSource.index !== 0
                && navData.hasLoc
                && navData.locCourse !== null
                && navData.deviation !== null
                && Math.abs(navData.deviation) < 1;
        }
        /**
         * Calculates an intercept angle, in degrees, to capture the desired track from a navigation signal using default
         * logic tuned for slow GA airplanes.
         * @param distanceToSource The distance from the plane to the source of the navigation signal, in nautical miles.
         * @param deflection The lateral deflection of the desired track relative to the plane, normalized from `-1` to `1`.
         * Positive values indicate that the desired track is to the right of the plane.
         * @param xtk The cross-track error of the plane from the desired track, in nautical miles. Positive values indicate
         * that the plane is to the right of the track.
         * @returns The intercept angle, in degrees, to capture the desired track from the navigation signal.
         */
        static defaultLateralInterceptCurve(distanceToSource, deflection, xtk) {
            let absInterceptAngle = Math.min(Math.pow(Math.abs(xtk) * 20, 1.35) + (Math.abs(xtk) * 50), 45);
            if (absInterceptAngle <= 2.5) {
                absInterceptAngle = NavMath.clamp(Math.abs(xtk * 150), 0, 2.5);
            }
            return absInterceptAngle;
        }
    }

    /**
     * An autopilot wing leveler director.
     */
    class APLvlDirector {
        /**
         * Creates an instance of the wing leveler.
         * @param apValues Autopilot values from this director's parent autopilot.
         * @param options Options to configure the new director.
         */
        constructor(apValues, options) {
            var _a;
            this.apValues = apValues;
            const bankRateOpt = options === null || options === void 0 ? void 0 : options.bankRate;
            switch (typeof bankRateOpt) {
                case 'number':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt * this.apValues.simRate.get());
                        }
                    };
                    break;
                case 'function':
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank, bankRateOpt() * this.apValues.simRate.get());
                        }
                    };
                    break;
                default:
                    this.driveBankFunc = bank => {
                        if (isFinite(bank) && this.driveBank) {
                            this.driveBank(bank);
                        }
                    };
            }
            this.omitWingLeveler = (_a = options === null || options === void 0 ? void 0 : options.omitWingLeveler) !== null && _a !== void 0 ? _a : false;
            this.state = exports.DirectorState.Inactive;
        }
        /**
         * Activates this director.
         */
        activate() {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            if (!this.omitWingLeveler) {
                SimVar.SetSimVarValue('AUTOPILOT WING LEVELER', 'Bool', true);
            }
        }
        /**
         * Arms this director.
         * This director has no armed mode, so it activates immediately.
         */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /**
         * Deactivates this director.
         */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            if (!this.omitWingLeveler) {
                SimVar.SetSimVarValue('AUTOPILOT WING LEVELER', 'Bool', false);
            }
        }
        /**
         * Updates this director.
         */
        update() {
            if (this.state === exports.DirectorState.Active) {
                this.driveBankFunc(0);
            }
        }
    }

    /**
     * A VNAV Path autopilot director.
     */
    class APVNavPathDirector {
        /**
         * Creates a new instance of APVNavPathDirector.
         * @param bus The event bus.
         * @param options Options with which to configure the director.
         */
        constructor(bus, options) {
            var _a;
            this.verticalWindAverage = new SimpleMovingAverage(10);
            this.deviationSimVar = exports.VNavVars.VerticalDeviation;
            this.fpaSimVar = exports.VNavVars.FPA;
            if (options === null || options === void 0 ? void 0 : options.guidance) {
                this.guidance = options.guidance;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.isGuidanceValidFunc = () => this.guidance.get().isValid;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.getFpaFunc = () => this.guidance.get().fpa;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                this.getDeviationFunc = () => this.guidance.get().deviation;
            }
            else {
                this.vnavIndex = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.vnavIndex) !== null && _a !== void 0 ? _a : 0, true);
                this.vnavIndex.sub(index => {
                    if (VNavUtils.isValidVNavIndex(index)) {
                        const suffix = index === 0 ? '' : `:${index}`;
                        this.deviationSimVar = `${exports.VNavVars.VerticalDeviation}${suffix}`;
                        this.fpaSimVar = `${exports.VNavVars.FPA}${suffix}`;
                    }
                });
                this.isGuidanceValidFunc = () => true;
                this.getFpaFunc = SimVar.GetSimVarValue.bind(undefined, this.fpaSimVar, exports.SimVarValueType.Degree);
                this.getDeviationFunc = SimVar.GetSimVarValue.bind(undefined, this.deviationSimVar, exports.SimVarValueType.Feet);
            }
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritDoc */
        activate() {
            this.state = exports.DirectorState.Active;
            SimVar.SetSimVarValue('AUTOPILOT PITCH HOLD', 'Bool', 0);
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
        }
        /** @inheritDoc */
        arm() {
            if (this.state === exports.DirectorState.Inactive) {
                this.state = exports.DirectorState.Armed;
                if (this.onArm !== undefined) {
                    this.onArm();
                }
            }
        }
        /** @inheritDoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            this.onDeactivate && this.onDeactivate();
        }
        /** @inheritDoc */
        update() {
            if (this.state === exports.DirectorState.Active) {
                if (!this.isGuidanceValidFunc()) {
                    this.deactivate();
                    return;
                }
                this.drivePitch && this.drivePitch(this.getDesiredPitch(), true, true);
            }
        }
        /**
         * Gets a desired pitch from the FPA, AOA and Deviation.
         * @returns The desired pitch angle.
         */
        getDesiredPitch() {
            // FPA uses positive-down convention.
            const fpa = this.getFpaFunc();
            // Deviation is positive if the path lies above the airplane.
            const deviation = this.getDeviationFunc();
            const groundSpeed = SimVar.GetSimVarValue('GROUND VELOCITY', exports.SimVarValueType.Knots);
            const fpaPercentage = Math.max(deviation / (VNavUtils.getPathErrorDistance(groundSpeed) * -1), -1) + 1;
            // We limit desired pitch to avoid divebombing if something like a flight plan change suddenly puts you way above the path
            return Math.min(MathUtils.clamp(fpa * fpaPercentage, -1, fpa + 3), 10);
        }
    }

    /**
     * An autopilot TOGA Pitch Director to be used for either a vertical TO or GA mode.
     */
    class APTogaPitchDirector {
        // eslint-disable-next-line jsdoc/require-jsdoc
        constructor(arg1, options) {
            var _a;
            let apValues = undefined;
            if (arg1 !== undefined && typeof arg1 === 'object') {
                apValues = arg1;
            }
            else {
                options = {
                    targetPitchAngle: arg1
                };
            }
            const targetPitchAngleOpt = (_a = options === null || options === void 0 ? void 0 : options.targetPitchAngle) !== null && _a !== void 0 ? _a : 10;
            if (typeof targetPitchAngleOpt === 'number') {
                this.targetPitchAngleFunc = () => targetPitchAngleOpt;
            }
            else {
                this.targetPitchAngleFunc = targetPitchAngleOpt;
            }
            if ((options === null || options === void 0 ? void 0 : options.drivePitch) && apValues) {
                const pitchRateOpt = options === null || options === void 0 ? void 0 : options.pitchRate;
                switch (typeof pitchRateOpt) {
                    case 'number':
                        this.drivePitchFunc = pitch => {
                            if (isFinite(pitch) && this.drivePitch) {
                                this.drivePitch(pitch, false, false, pitchRateOpt * apValues.simRate.get());
                            }
                        };
                        break;
                    case 'function':
                        this.drivePitchFunc = pitch => {
                            if (isFinite(pitch) && this.drivePitch) {
                                this.drivePitch(pitch, false, false, pitchRateOpt() * apValues.simRate.get());
                            }
                        };
                        break;
                    default:
                        this.drivePitchFunc = pitch => {
                            if (isFinite(pitch) && this.drivePitch) {
                                this.drivePitch(pitch);
                            }
                        };
                }
            }
            this.isConstantPitch = typeof targetPitchAngleOpt === 'number' && this.drivePitchFunc === undefined;
            this.state = exports.DirectorState.Inactive;
        }
        /** @inheritDoc */
        activate() {
            this.state = exports.DirectorState.Active;
            if (this.onActivate !== undefined) {
                this.onActivate();
            }
            if (this.isConstantPitch) {
                this.setPitch && this.setPitch(-this.targetPitchAngleFunc());
            }
            // TODO: The simvar is not currently writeable, so the line below has no effect.
            SimVar.SetSimVarValue('AUTOPILOT TAKEOFF POWER ACTIVE', 'Bool', true);
            SimVar.SetSimVarValue('L:WT_TOGA_ACTIVE', 'Bool', true);
        }
        /** @inheritDoc */
        arm() {
            if (this.state == exports.DirectorState.Inactive) {
                this.activate();
            }
        }
        /** @inheritDoc */
        deactivate() {
            this.state = exports.DirectorState.Inactive;
            // TODO: The simvar is not currently writeable, so the line below has no effect.
            SimVar.SetSimVarValue('AUTOPILOT TAKEOFF POWER ACTIVE', 'Bool', false);
            SimVar.SetSimVarValue('L:WT_TOGA_ACTIVE', 'Bool', false);
        }
        /** @inheritDoc */
        update() {
            if (!this.isConstantPitch) {
                if (this.drivePitchFunc) {
                    this.drivePitchFunc(-this.targetPitchAngleFunc());
                }
                else if (this.setPitch) {
                    this.setPitch(-this.targetPitchAngleFunc());
                }
            }
        }
    }

    /**
     * Handles the calculation of a Glide Path.
     */
    class GlidePathCalculator {
        /**
         * Creates a new instance of GlidePathCalculator.
         * @param bus The event bus.
         * @param flightPlanner The flight planner to use with this instance.
         * @param primaryPlanIndex The primary plan index to use for calculating GlidePath.
         */
        constructor(bus, flightPlanner, primaryPlanIndex) {
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.primaryPlanIndex = primaryPlanIndex;
            this.mapLegIndex = 0;
            this.fafLegIndex = 0;
            this.planePos = new GeoPoint(0, 0);
            this.glidepathFpa = 0;
            this.flightPlanner.onEvent('fplCopied').handle(e => {
                if (e.targetPlanIndex === this.primaryPlanIndex) {
                    this.onPlanChanged();
                }
            });
            this.flightPlanner.onEvent('fplCreated').handle(e => {
                if (e.planIndex === this.primaryPlanIndex) {
                    this.onPlanChanged();
                }
            });
            this.flightPlanner.onEvent('fplLegChange').handle(e => {
                if (e.planIndex === this.primaryPlanIndex) {
                    this.onPlanChanged();
                }
            });
            this.flightPlanner.onEvent('fplLoaded').handle(e => {
                if (e.planIndex === this.primaryPlanIndex) {
                    this.onPlanChanged();
                }
            });
            this.flightPlanner.onEvent('fplSegmentChange').handle(e => {
                if (e.planIndex === this.primaryPlanIndex) {
                    this.onPlanChanged();
                }
            });
            this.flightPlanner.onEvent('fplIndexChanged').handle(() => this.onPlanChanged());
            this.flightPlanner.onEvent('fplCalculated').handle(e => {
                if (e.planIndex === this.primaryPlanIndex) {
                    this.onPlanCalculated();
                }
            });
            const gnss = this.bus.getSubscriber();
            gnss.on('gps-position').handle(lla => {
                this.planePos.set(lla.lat, lla.long);
            });
        }
        /**
         * Responds to when the primary flight plan changes.
         */
        onPlanChanged() {
            if (this.flightPlanner.hasFlightPlan(this.primaryPlanIndex)) {
                const plan = this.flightPlanner.getFlightPlan(this.primaryPlanIndex);
                this.mapLegIndex = VNavUtils.getMissedApproachLegIndex(plan);
                const faf = VNavUtils.getFafIndex(plan);
                this.fafLegIndex = faf !== undefined ? faf : Math.max(0, plan.length - 1);
            }
        }
        /**
         * Responds to when the primary flight plan's lateral flight path vectors are calculated.
         */
        onPlanCalculated() {
            if (this.flightPlanner.hasFlightPlan(this.primaryPlanIndex)) {
                const plan = this.flightPlanner.getFlightPlan(this.primaryPlanIndex);
                this.calcGlidepathFpa(plan);
            }
        }
        /**
         * Gets the current Glidepath distance in meters.
         * @param index The global index of the active leg.
         * @param distanceAlongLeg The aircraft's current distance along the active leg, in meters.
         * @returns The current Glidepath distance in meters.
         */
        getGlidepathDistance(index, distanceAlongLeg) {
            var _a, _b;
            let globalLegIndex = 0;
            let distance = 0;
            const plan = this.flightPlanner.getFlightPlan(this.primaryPlanIndex);
            const destLeg = plan.getLeg(this.mapLegIndex);
            if (index <= this.mapLegIndex) {
                for (let segmentIndex = 0; segmentIndex < plan.segmentCount; segmentIndex++) {
                    const segment = plan.getSegment(segmentIndex);
                    for (let legIndex = 0; legIndex < segment.legs.length; legIndex++) {
                        const leg = segment.legs[legIndex];
                        if (leg.calculated !== undefined && globalLegIndex <= this.mapLegIndex) {
                            if (index === globalLegIndex) {
                                distance += ((_a = leg.calculated) === null || _a === void 0 ? void 0 : _a.distanceWithTransitions) - distanceAlongLeg;
                            }
                            else if (globalLegIndex > index) {
                                distance += (_b = leg.calculated) === null || _b === void 0 ? void 0 : _b.distanceWithTransitions;
                            }
                        }
                        globalLegIndex++;
                    }
                }
                if (ICAO.isFacility(destLeg.leg.fixIcao)
                    && ICAO.getFacilityType(destLeg.leg.fixIcao) !== exports.FacilityType.RWY
                    && plan.procedureDetails.destinationRunway !== undefined
                    && destLeg.calculated && destLeg.calculated.endLat !== undefined && destLeg.calculated.endLon !== undefined) {
                    const runway = plan.procedureDetails.destinationRunway;
                    const runwayGeoPoint = new GeoPoint(runway.latitude, runway.longitude);
                    if (index === this.mapLegIndex && (distanceAlongLeg >= (destLeg.calculated.distanceWithTransitions - 1))) {
                        const destEnd = new GeoPoint(destLeg.calculated.endLat, destLeg.calculated.endLon);
                        distance = UnitType.NMILE.convertTo(NavMath.alongTrack(runwayGeoPoint, destEnd, this.planePos), UnitType.METER);
                    }
                    else {
                        distance += UnitType.GA_RADIAN.convertTo(runwayGeoPoint.distance(destLeg.calculated.endLat, destLeg.calculated.endLon), UnitType.METER);
                    }
                }
            }
            return distance;
        }
        /**
         * Gets the Glidepath desired altitude in meters.
         * @param distance The current Glidepath distance in meters.
         * @returns The current Glidepath desired altitude in meters.
         */
        getDesiredGlidepathAltitude(distance) {
            return this.getRunwayAltitude() + VNavUtils.altitudeForDistance(this.glidepathFpa, distance);
        }
        /**
         * Gets the Glidepath runway altitude in meters.
         * @returns The Glidepath runway altitude in meters.
         */
        getRunwayAltitude() {
            const plan = this.flightPlanner.getFlightPlan(this.primaryPlanIndex);
            const destLeg = plan.getLeg(this.mapLegIndex);
            let destAltitude = destLeg.leg.altitude1;
            if (ICAO.isFacility(destLeg.leg.fixIcao)
                && ICAO.getFacilityType(destLeg.leg.fixIcao) !== exports.FacilityType.RWY
                && plan.procedureDetails.destinationRunway !== undefined) {
                destAltitude = plan.procedureDetails.destinationRunway.elevation;
            }
            return destAltitude;
        }
        /**
         * Calculates the Glidepath flight path angle using the destination elevation
         * and FAF altitude restriction.
         * @param plan The plan to calculate from.
         */
        calcGlidepathFpa(plan) {
            if (plan.length < 2 || this.fafLegIndex > plan.length || this.mapLegIndex > plan.length) {
                return;
            }
            const fafLeg = plan.tryGetLeg(this.fafLegIndex);
            const destLeg = plan.tryGetLeg(this.mapLegIndex);
            if (!fafLeg || !destLeg) {
                return;
            }
            let fafToDestDistance = 0;
            for (let i = this.fafLegIndex + 1; i <= this.mapLegIndex; i++) {
                const leg = plan.getLeg(i);
                if (leg.calculated !== undefined) {
                    fafToDestDistance += leg.calculated.distance;
                }
            }
            let destAltitude = destLeg.leg.altitude1;
            if (ICAO.isFacility(destLeg.leg.fixIcao)
                && ICAO.getFacilityType(destLeg.leg.fixIcao) !== exports.FacilityType.RWY
                && plan.procedureDetails.destinationRunway !== undefined
                && destLeg.calculated && destLeg.calculated.endLat !== undefined && destLeg.calculated.endLon !== undefined) {
                const runway = plan.procedureDetails.destinationRunway;
                const runwayGeoPoint = new GeoPoint(runway.latitude, runway.longitude);
                destAltitude = runway.elevation;
                fafToDestDistance += UnitType.GA_RADIAN.convertTo(runwayGeoPoint.distance(destLeg.calculated.endLat, destLeg.calculated.endLon), UnitType.METER);
            }
            this.glidepathFpa = VNavUtils.getFpa(fafToDestDistance, fafLeg.leg.altitude1 - destAltitude);
        }
    }

    /**
     * Handles the calculation of the VNAV flight path for Path Smoothing VNAV Implementations.
     */
    class SmoothingPathCalculator {
        /**
         * Creates an instance of SmoothingPathCalculator.
         * @param bus The EventBus to use with this instance.
         * @param flightPlanner The flight planner to use with this instance.
         * @param primaryPlanIndex The primary flight plan index to use to calculate a path from.
         * @param options Options for the calculator.
         */
        constructor(bus, flightPlanner, primaryPlanIndex, options) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.primaryPlanIndex = primaryPlanIndex;
            /** The Vertical Flight Plans managed by this Path Calculator */
            this.verticalFlightPlans = [];
            /** @inheritdoc */
            this.planBuilt = new SubEvent();
            /** @inheritdoc */
            this.vnavCalculated = new SubEvent();
            this.legAltitudes = [0, 0];
            this.applyPathValuesResult = [undefined, 0];
            this.index = (_a = options === null || options === void 0 ? void 0 : options.index) !== null && _a !== void 0 ? _a : 0;
            this.flightPathAngle = (_b = options === null || options === void 0 ? void 0 : options.defaultFpa) !== null && _b !== void 0 ? _b : SmoothingPathCalculator.DEFAULT_DEFAULT_FPA;
            this.minFlightPathAngle = (_c = options === null || options === void 0 ? void 0 : options.minFpa) !== null && _c !== void 0 ? _c : SmoothingPathCalculator.DEFAULT_MIN_FPA;
            this.maxFlightPathAngle = (_d = options === null || options === void 0 ? void 0 : options.maxFpa) !== null && _d !== void 0 ? _d : SmoothingPathCalculator.DEFAULT_MAX_FPA;
            this.forceFirstApproachAtConstraint = (_e = options === null || options === void 0 ? void 0 : options.forceFirstApproachAtConstraint) !== null && _e !== void 0 ? _e : false;
            this.directToLegOffset = (_f = options === null || options === void 0 ? void 0 : options.directToLegOffset) !== null && _f !== void 0 ? _f : SmoothingPathCalculator.DEFAULT_DIRECT_TO_LEG_OFFSET;
            this.isLegEligibleFunc = (_g = options === null || options === void 0 ? void 0 : options.isLegEligible) !== null && _g !== void 0 ? _g : SmoothingPathCalculator.isLegVnavEligible;
            this.shouldUseConstraintFunc = (_h = options === null || options === void 0 ? void 0 : options.shouldUseConstraint) !== null && _h !== void 0 ? _h : (() => true);
            this.invalidateClimbConstraintFunc = (_j = options === null || options === void 0 ? void 0 : options.invalidateClimbConstraint) !== null && _j !== void 0 ? _j : SmoothingPathCalculator.invalidateClimbConstraint;
            this.invalidateDescentConstraintFunc = (_k = options === null || options === void 0 ? void 0 : options.invalidateDescentConstraint) !== null && _k !== void 0 ? _k : SmoothingPathCalculator.invalidateDescentConstraint;
            this.flightPlanner.onEvent('fplCreated').handle(e => this.createVerticalPlan(e.planIndex));
            this.flightPlanner.onEvent('fplCopied').handle(e => this.onPlanChanged(e.targetPlanIndex));
            this.flightPlanner.onEvent('fplLoaded').handle(e => this.onPlanChanged(e.planIndex));
            this.flightPlanner.onEvent('fplLegChange').handle(e => this.onPlanChanged(e.planIndex, e));
            this.flightPlanner.onEvent('fplSegmentChange').handle(e => this.onPlanChanged(e.planIndex, undefined, e));
            this.flightPlanner.onEvent('fplIndexChanged').handle(e => this.onPlanChanged(e.planIndex));
            this.flightPlanner.onEvent('fplCalculated').handle(e => this.onPlanCalculated(e));
            const vnavTopicSuffix = VNavUtils.getEventBusTopicSuffix(this.index);
            const sub = bus.getSubscriber();
            sub.on(`vnav_set_default_fpa${vnavTopicSuffix}`).handle(this.setDefaultFpa.bind(this));
            sub.on(`vnav_set_vnav_direct_to${vnavTopicSuffix}`).handle(data => {
                if (data.globalLegIndex < 0) {
                    this.cancelVerticalDirect(data.planIndex);
                }
                else {
                    this.activateVerticalDirect(data.planIndex, data.globalLegIndex, data.fpa);
                }
            });
        }
        /** @inheritdoc */
        getVerticalFlightPlan(planIndex) {
            var _a;
            var _b;
            return (_a = (_b = this.verticalFlightPlans)[planIndex]) !== null && _a !== void 0 ? _a : (_b[planIndex] = this.createVerticalPlan(planIndex));
        }
        /** @inheritdoc */
        createVerticalPlan(planIndex) {
            const verticalFlightPlan = {
                planIndex,
                length: 0,
                constraints: [],
                segments: [],
                destLegIndex: undefined,
                fafLegIndex: undefined,
                firstDescentConstraintLegIndex: undefined,
                lastDescentConstraintLegIndex: undefined,
                missedApproachStartIndex: undefined,
                currentAlongLegDistance: undefined,
                verticalDirectIndex: undefined,
                verticalDirectFpa: undefined,
                planChanged: true
            };
            this.verticalFlightPlans[planIndex] = verticalFlightPlan;
            return verticalFlightPlan;
        }
        /** @inheritdoc */
        requestPathCompute(planIndex) {
            if (this.flightPlanner.hasFlightPlan(planIndex) && this.verticalFlightPlans[planIndex] !== undefined) {
                const lateralPlan = this.flightPlanner.getFlightPlan(planIndex);
                const verticalPlan = this.getVerticalFlightPlan(planIndex);
                this.computePathAndNotify(lateralPlan, verticalPlan);
                return true;
            }
            return false;
        }
        /**
         * Gets the index of the VNAV constraint defining the target VNAV altitude for a flight plan leg.
         * @param planIndex The flight plan index.
         * @param globalLegIndex The global index of the flight plan leg.
         * @returns The index of the VNAV constraint defining the target VNAV altitude for a flight plan leg, or `-1` if one
         * could not be found.
         */
        getTargetConstraintIndex(planIndex, globalLegIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            if (this.getFlightPhase(planIndex) === exports.VerticalFlightPhase.Descent) {
                const currentConstraint = VNavUtils.getPriorConstraintFromLegIndex(verticalPlan, globalLegIndex);
                if (currentConstraint && currentConstraint.nextVnavEligibleLegIndex !== undefined && globalLegIndex < currentConstraint.nextVnavEligibleLegIndex) {
                    const priorConstraintIndex = VNavUtils.getPriorConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
                    const priorConstraint = verticalPlan.constraints[priorConstraintIndex];
                    if (priorConstraint && priorConstraint.type !== 'climb' && priorConstraint.type !== 'missed') {
                        return priorConstraintIndex;
                    }
                    else {
                        return -1;
                    }
                }
                let i = verticalPlan.constraints.length - 1;
                while (i >= 0) {
                    const constraint = verticalPlan.constraints[i];
                    if (globalLegIndex <= constraint.index && constraint.isTarget && constraint.type !== 'climb' && constraint.type !== 'missed') {
                        return i;
                    }
                    i--;
                }
            }
            else {
                const currentConstraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
                if (currentConstraintIndex >= 0) {
                    const currentConstraint = verticalPlan.constraints[currentConstraintIndex];
                    const isMissed = currentConstraint.type === 'missed';
                    for (let i = currentConstraintIndex; i >= 0; i--) {
                        const constraint = verticalPlan.constraints[i];
                        if (constraint.type === 'climb' || (isMissed && constraint.type === 'missed')) {
                            if (constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                                return i;
                            }
                        }
                        else {
                            return -1;
                        }
                    }
                }
            }
            return -1;
        }
        /**
         * Gets the VNAV constraint defining the target VNAV altitude for a flight plan leg.
         * @param planIndex The flight plan index.
         * @param globalLegIndex The global index of the flight plan leg.
         * @returns The VNAV constraint defining the target VNAV altitude for a flight plan leg, or `undefined` if one could
         * not be found.
         */
        getTargetConstraint(planIndex, globalLegIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            return verticalPlan.constraints[this.getTargetConstraintIndex(planIndex, globalLegIndex)];
        }
        /** @inheritdoc */
        getTargetAltitude(planIndex, globalLegIndex) {
            var _a, _b;
            if (this.getFlightPhase(planIndex) === exports.VerticalFlightPhase.Descent) {
                return (_a = this.getTargetConstraint(planIndex, globalLegIndex)) === null || _a === void 0 ? void 0 : _a.targetAltitude;
            }
            else {
                return (_b = this.getTargetConstraint(planIndex, globalLegIndex)) === null || _b === void 0 ? void 0 : _b.maxAltitude;
            }
        }
        /** @inheritdoc */
        getFlightPhase(planIndex) {
            if (this.flightPlanner.hasFlightPlan(planIndex)) {
                const lateralPlan = this.flightPlanner.getFlightPlan(planIndex);
                const verticalPlan = this.getVerticalFlightPlan(planIndex);
                const globalLegIndex = VNavUtils.getConstraintLegIndexFromLegIndex(verticalPlan, lateralPlan.activeLateralLeg);
                if (globalLegIndex > -1) {
                    const constraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
                    switch (constraint === null || constraint === void 0 ? void 0 : constraint.type) {
                        case 'climb':
                        case 'missed':
                            return exports.VerticalFlightPhase.Climb;
                    }
                }
            }
            return exports.VerticalFlightPhase.Descent;
        }
        /** @inheritdoc */
        getCurrentConstraintAltitude(planIndex, globalLegIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint === undefined) {
                return undefined;
            }
            const priorConstraint = VNavUtils.getPriorConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint.type !== 'climb' && currentConstraint.type !== 'missed'
                && currentConstraint.nextVnavEligibleLegIndex !== undefined
                && globalLegIndex < currentConstraint.nextVnavEligibleLegIndex) {
                return priorConstraint === null || priorConstraint === void 0 ? void 0 : priorConstraint.targetAltitude;
            }
            else {
                return currentConstraint.targetAltitude;
            }
        }
        /** @inheritdoc */
        getCurrentConstraintDetails(planIndex, globalLegIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint === undefined) {
                return { type: exports.AltitudeRestrictionType.Unused, altitude: 0 };
            }
            const priorConstraint = VNavUtils.getPriorConstraintFromLegIndex(verticalPlan, globalLegIndex);
            if (currentConstraint.type !== 'climb' && currentConstraint.type !== 'missed'
                && currentConstraint.nextVnavEligibleLegIndex !== undefined
                && globalLegIndex < currentConstraint.nextVnavEligibleLegIndex) {
                if (priorConstraint) {
                    return VNavUtils.getConstraintDetails(priorConstraint, { type: exports.AltitudeRestrictionType.Unused, altitude: 0 });
                }
                else {
                    return { type: exports.AltitudeRestrictionType.Unused, altitude: 0 };
                }
            }
            else {
                return VNavUtils.getConstraintDetails(currentConstraint, { type: exports.AltitudeRestrictionType.Unused, altitude: 0 });
            }
        }
        /** @inheritdoc */
        getNextConstraintAltitude(planIndex, globalLegIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, globalLegIndex);
            // added check for climb or descent for smoothing path calc
            if (currentConstraint !== undefined) {
                if (this.getFlightPhase(planIndex) === exports.VerticalFlightPhase.Climb) {
                    if (currentConstraint.maxAltitude < Number.POSITIVE_INFINITY) {
                        return currentConstraint.maxAltitude;
                    }
                    else {
                        return currentConstraint.minAltitude;
                    }
                }
                else {
                    if (currentConstraint.minAltitude > Number.NEGATIVE_INFINITY) {
                        return currentConstraint.minAltitude;
                    }
                    else {
                        return currentConstraint.maxAltitude;
                    }
                }
            }
            return undefined;
        }
        /** @inheritdoc */
        getNextRestrictionForFlightPhase(planIndex, activeLateralLeg) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            const currentConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, activeLateralLeg);
            if (currentConstraint) {
                const currentConstraintIndex = verticalPlan.constraints.indexOf(currentConstraint);
                if (currentConstraintIndex > -1) {
                    if (this.getFlightPhase(planIndex) === exports.VerticalFlightPhase.Climb) {
                        for (let i = currentConstraintIndex; i >= 0; i--) {
                            const constraint = verticalPlan.constraints[i];
                            if (constraint.type === 'climb' || constraint.type === 'missed') {
                                if (constraint.minAltitude > Number.NEGATIVE_INFINITY) {
                                    return constraint;
                                }
                            }
                            else {
                                return undefined;
                            }
                        }
                    }
                    else {
                        for (let i = currentConstraintIndex; i >= 0; i--) {
                            const constraint = verticalPlan.constraints[i];
                            if (constraint.type === 'descent' || constraint.type === 'direct' || constraint.type === 'manual') {
                                if (constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                                    return constraint;
                                }
                            }
                            else {
                                return undefined;
                            }
                        }
                    }
                }
            }
            return undefined;
        }
        /** @inheritdoc */
        activateVerticalDirect(planIndex, constraintGlobalLegIndex, fpa) {
            if (constraintGlobalLegIndex < 0) {
                return;
            }
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            verticalPlan.verticalDirectIndex = constraintGlobalLegIndex;
            verticalPlan.verticalDirectFpa = fpa !== null && fpa !== void 0 ? fpa : this.flightPathAngle;
            const lateralPlan = this.flightPlanner.getFlightPlan(planIndex);
            this.buildVerticalFlightPlanAndNotify(lateralPlan, verticalPlan);
            this.computePathAndNotify(lateralPlan, verticalPlan);
        }
        /**
         * Cancels the existing VNAV direct-to for a vertical flight plan.
         * @param planIndex The index of the vertical flight plan for which to cancel the VNAV direct-to.
         */
        cancelVerticalDirect(planIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            if (verticalPlan.verticalDirectIndex === undefined) {
                return;
            }
            verticalPlan.verticalDirectIndex = undefined;
            verticalPlan.verticalDirectFpa = undefined;
            const lateralPlan = this.flightPlanner.getFlightPlan(planIndex);
            this.buildVerticalFlightPlanAndNotify(lateralPlan, verticalPlan);
            this.computePathAndNotify(lateralPlan, verticalPlan);
        }
        /**
         * Sets this calculator's default flight path angle.
         * @param fpa The new default flight path angle, in degrees. Increasingly positive values indicate steeper descents.
         */
        setDefaultFpa(fpa) {
            const newFpa = Math.max(0, fpa);
            if (newFpa !== this.flightPathAngle) {
                this.flightPathAngle = newFpa;
                for (let i = 0; i < this.verticalFlightPlans.length; i++) {
                    const lateralPlan = this.flightPlanner.hasFlightPlan(i) ? this.flightPlanner.getFlightPlan(i) : undefined;
                    const verticalPlan = this.verticalFlightPlans[i];
                    if (lateralPlan && verticalPlan) {
                        this.computePathAndNotify(lateralPlan, verticalPlan);
                    }
                }
            }
        }
        /**
         * Sets planChanged to true to flag that a plan change has been received over the bus.
         * @param planIndex The Plan Index that changed.
         * @param legChangeEvent The FlightPlanLegEvent, if any.
         * @param segmentChangeEvent The FlightPlanSegmentEvent, if any.
         */
        onPlanChanged(planIndex, legChangeEvent, segmentChangeEvent) {
            const plan = this.flightPlanner.getFlightPlan(planIndex);
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            if (verticalPlan.verticalDirectIndex !== undefined) {
                if (legChangeEvent !== undefined) {
                    const globalIndex = plan.getSegment(legChangeEvent.segmentIndex).offset + legChangeEvent.legIndex;
                    if (globalIndex <= verticalPlan.verticalDirectIndex) {
                        verticalPlan.verticalDirectIndex = undefined;
                    }
                }
                else if (segmentChangeEvent !== undefined) {
                    const verticalDirectSegmentIndex = plan.getSegmentIndex(verticalPlan.verticalDirectIndex);
                    if (segmentChangeEvent.segmentIndex <= verticalDirectSegmentIndex) {
                        verticalPlan.verticalDirectIndex = undefined;
                    }
                }
            }
            verticalPlan.planChanged = true;
            verticalPlan.currentAlongLegDistance = undefined;
        }
        /**
         * Method fired on a flight plan change event to rebuild the vertical path.
         * @param event The Flight Plan Calculated Event
         */
        onPlanCalculated(event) {
            this.buildVerticalFlightPlanAndComputeAndNotify(event.planIndex);
        }
        /**
         * Builds a vertical flight plan if its corresponding lateral flight plan has been changed since the last rebuild,
         * then computes the vertical path sends events notifying subscribers that the plan was built and calculated.
         * @param planIndex The index of the plan to build and compute.
         */
        buildVerticalFlightPlanAndComputeAndNotify(planIndex) {
            const lateralPlan = this.flightPlanner.getFlightPlan(planIndex);
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            if (verticalPlan.planChanged) {
                this.buildVerticalFlightPlanAndNotify(lateralPlan, verticalPlan);
            }
            this.computePathAndNotify(lateralPlan, verticalPlan);
        }
        /**
         * Sends an event notifying subscribers that a vertical flight plan was built or rebuilt.
         * @param planIndex The index of the plan that was built.
         */
        notifyBuilt(planIndex) {
            this.planBuilt.notify(this, planIndex);
        }
        /**
         * Sends an event notifying subscribers that a vertical flight plan was calculated.
         * @param planIndex The index of the plan that was calculated.
         */
        notifyCalculated(planIndex) {
            this.vnavCalculated.notify(this, planIndex);
        }
        /**
         * Builds a vertical flight plan from a lateral flight plan and sends an event notifying subscribers that the plan
         * was built.
         * @param lateralPlan The lateral flight plan.
         * @param verticalPlan The vertical flight plan to build.
         */
        buildVerticalFlightPlanAndNotify(lateralPlan, verticalPlan) {
            this.buildVerticalFlightPlan(lateralPlan, verticalPlan);
            this.notifyBuilt(verticalPlan.planIndex);
        }
        /**
         * Builds a vertical flight plan from a lateral flight plan.
         * @param lateralPlan The lateral flight plan.
         * @param verticalPlan The vertical flight plan to build.
         */
        buildVerticalFlightPlan(lateralPlan, verticalPlan) {
            this.buildVerticalLegsAndConstraints(lateralPlan, verticalPlan);
            SmoothingPathCalculator.handleDirectToLegInVerticalPlan(lateralPlan, verticalPlan, this.directToLegOffset);
            verticalPlan.planChanged = false;
        }
        /**
         * Resets the Vertical Flight Plan, populates the vertical segments and legs, finds and builds the vertical constraints.
         * @param lateralPlan The Lateral Flight Plan.
         * @param verticalPlan The Vertical Flight Plan.
         */
        buildVerticalLegsAndConstraints(lateralPlan, verticalPlan) {
            var _a, _b, _c, _d, _e, _f, _g;
            // Reset the constraints array.
            verticalPlan.constraints.length = 0;
            // Reset the segments array.
            verticalPlan.segments.length = 0;
            verticalPlan.destLegIndex = undefined;
            verticalPlan.firstDescentConstraintLegIndex = undefined;
            verticalPlan.lastDescentConstraintLegIndex = undefined;
            verticalPlan.missedApproachStartIndex = undefined;
            // Find the FAF in the lateral plan, if any.
            verticalPlan.fafLegIndex = VNavUtils.getFafIndex(lateralPlan);
            const directToTargetLegIndex = SmoothingPathCalculator.getDirectToTargetLegIndex(lateralPlan);
            let firstApproachGlobalLegIndex;
            // Iterate forward through the lateral plan to build the constraints
            for (const segment of lateralPlan.segments()) {
                // Add the plan segments to the VNav Path Calculator Segments
                verticalPlan.segments[segment.segmentIndex] = {
                    offset: segment.offset,
                    legs: []
                };
                if (segment.segmentType === exports.FlightPlanSegmentType.Approach && firstApproachGlobalLegIndex === undefined) {
                    firstApproachGlobalLegIndex = segment.offset;
                }
                for (let segmentLegIndex = 0; segmentLegIndex < segment.legs.length; segmentLegIndex++) {
                    const globalLegIndex = segment.offset + segmentLegIndex;
                    const lateralLeg = segment.legs[segmentLegIndex];
                    const verticalLeg = VNavUtils.createLeg(segment.segmentIndex, segmentLegIndex, (_a = lateralLeg.name) !== null && _a !== void 0 ? _a : '', (_c = (_b = lateralLeg.calculated) === null || _b === void 0 ? void 0 : _b.distanceWithTransitions) !== null && _c !== void 0 ? _c : undefined);
                    // Check if the leg is part of the missed approach, and set the missed approach start index.
                    if (verticalPlan.missedApproachStartIndex === undefined
                        && segment.segmentType === exports.FlightPlanSegmentType.Approach
                        && BitFlags.isAll(lateralLeg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                        verticalPlan.missedApproachStartIndex = globalLegIndex;
                    }
                    // Check if the leg contains a constraint
                    const constraintAltitudes = SmoothingPathCalculator.getConstraintAltitudes(lateralLeg, this.legAltitudes);
                    verticalLeg.isEligible = this.isLegEligibleFunc(lateralLeg);
                    verticalLeg.distance = (_e = (_d = lateralLeg.calculated) === null || _d === void 0 ? void 0 : _d.distanceWithTransitions) !== null && _e !== void 0 ? _e : 0;
                    // Check if the leg precedes a defined vertical direct for this vertical flight plan.
                    const legPrecedesVerticalDirectIndex = verticalPlan.verticalDirectIndex !== undefined && globalLegIndex < verticalPlan.verticalDirectIndex;
                    const legPrecedesDirectTo = directToTargetLegIndex !== undefined && globalLegIndex < directToTargetLegIndex + this.directToLegOffset;
                    if (constraintAltitudes !== undefined
                        && !legPrecedesVerticalDirectIndex
                        && !legPrecedesDirectTo
                        && this.shouldUseConstraintFunc(lateralPlan, lateralLeg, globalLegIndex, segment, segmentLegIndex)) {
                        verticalLeg.isUserDefined = VNavUtils.isUserConstraint(lateralLeg);
                        const verticalConstraint = this.buildConstraint(verticalPlan, globalLegIndex, lateralLeg, constraintAltitudes, verticalLeg.name);
                        // Add the new vertical constraint to the array of constraints in reverse order.
                        verticalPlan.constraints.unshift(verticalConstraint);
                    }
                    // Add the new vertical leg to the vertical flight plan
                    verticalPlan.segments[segment.segmentIndex].legs.push(verticalLeg);
                }
            }
            verticalPlan.length = lateralPlan.length;
            if (this.forceFirstApproachAtConstraint && firstApproachGlobalLegIndex !== undefined) {
                const firstApproachConstraint = VNavUtils.getConstraintFromLegIndex(verticalPlan, directToTargetLegIndex === firstApproachGlobalLegIndex ? directToTargetLegIndex + 3 : firstApproachGlobalLegIndex);
                if (firstApproachConstraint && firstApproachConstraint.type !== 'climb' && firstApproachConstraint.type !== 'missed') {
                    SmoothingPathCalculator.forceAtConstraint(firstApproachConstraint);
                }
            }
            verticalPlan.firstDescentConstraintLegIndex = (_f = verticalPlan.constraints[VNavUtils.getFirstDescentConstraintIndex(verticalPlan)]) === null || _f === void 0 ? void 0 : _f.index;
            verticalPlan.lastDescentConstraintLegIndex = (_g = verticalPlan.constraints[VNavUtils.getLastDescentConstraintIndex(verticalPlan)]) === null || _g === void 0 ? void 0 : _g.index;
        }
        /**
         * Builds a VNAV constraint for a lateral flight plan leg.
         * @param verticalPlan The vertical flight plan.
         * @param globalLegIndex The global index of the lateral flight plan leg for which to build the constraint.
         * @param lateralLeg The lateral flight plan leg for which to build the constraint.
         * @param constraintAltitudes The constraint altitudes, as `[minimum_altitude, maximum_altitude]`.
         * @param name The name of the new constraint.
         * @returns A new VNAV constraint for the specified lateral flight plan leg.
         */
        buildConstraint(verticalPlan, globalLegIndex, lateralLeg, constraintAltitudes, name) {
            var _a;
            const constraint = VNavUtils.createConstraint(globalLegIndex, constraintAltitudes[0], constraintAltitudes[1], name, BitFlags.isAll(lateralLeg.flags, exports.LegDefinitionFlags.MissedApproach) ? 'missed' : lateralLeg.verticalData.phase === exports.VerticalFlightPhase.Descent ? 'descent' : 'climb');
            constraint.isBeyondFaf = verticalPlan.fafLegIndex === undefined ? false : globalLegIndex > verticalPlan.fafLegIndex;
            // Check if this constraint is a vertical direct.
            if (verticalPlan.verticalDirectIndex === globalLegIndex) {
                constraint.fpa = (_a = verticalPlan.verticalDirectFpa) !== null && _a !== void 0 ? _a : this.flightPathAngle;
                constraint.type = 'direct';
            }
            const userFpa = lateralLeg.verticalData.fpa;
            if (userFpa !== undefined && constraint.type !== 'climb' && constraint.type !== 'missed') {
                constraint.fpa = userFpa;
                constraint.type = 'manual';
            }
            return constraint;
        }
        /**
         * Computes the vertical path for a flight plan and sends an event notifying subscribers that the plan was
         * calculated.
         * @param lateralPlan The lateral flight plan for which to compute a path.
         * @param verticalPlan The vertical flight plan for which to compute a path.
         */
        computePathAndNotify(lateralPlan, verticalPlan) {
            this.computePath(lateralPlan, verticalPlan);
            this.notifyCalculated(lateralPlan.planIndex);
        }
        /**
         * Computes the vertical path for a flight plan.
         * @param lateralPlan The lateral flight plan for which to compute a path.
         * @param verticalPlan The vertical flight plan for which to compute a path.
         */
        computePath(lateralPlan, verticalPlan) {
            this.computeDescentPath(lateralPlan, verticalPlan);
        }
        /**
         * Computes the descent path for a flight plan.
         * @param lateralPlan The lateral flight plan for which to compute a path.
         * @param verticalPlan The vertical flight plan for which to compute a path.
         */
        computeDescentPath(lateralPlan, verticalPlan) {
            this.fillLegDistances(lateralPlan, verticalPlan);
            // Updated leg distances could cause some invalidated constraints to become valid, so we will re-insert all
            // invalidated constraints and filter them again.
            this.reinsertInvalidConstraints(verticalPlan, lateralPlan);
            this.findAndRemoveInvalidConstraints(verticalPlan);
            if (verticalPlan.constraints.length < 1) {
                return;
            }
            this.populateConstraints(verticalPlan);
            if (this.computeFlightPathAngles(verticalPlan)) {
                for (let constraintIndex = 0; constraintIndex < verticalPlan.constraints.length; constraintIndex++) {
                    const constraint = verticalPlan.constraints[constraintIndex];
                    if (constraint.type === 'descent' || constraint.type === 'direct' || constraint.type === 'manual') {
                        let altitude = constraint.targetAltitude;
                        let constraintIsBod = true;
                        // Check to see if the current constraint is not considered a BOD constraint. A constraint is not BOD if and
                        // only if there is a following constraint (in flight plan order) that is not a climb constraint and is
                        // path-eligible, and one of the following is true:
                        // - the following constraint is not flat (has FPA > 0) and the computed vertical path to the following
                        // constraint is not above the path ending at the current constraint at the location of the current
                        // constraint (with a 25-meter margin).
                        // - the current constraint is flat (has FPA = 0).
                        if (constraintIndex > 0) {
                            const followingConstraint = verticalPlan.constraints[constraintIndex - 1];
                            if (followingConstraint.type !== 'climb'
                                && followingConstraint.type !== 'missed'
                                && followingConstraint.nextVnavEligibleLegIndex === undefined) {
                                const constraintAltForDist = followingConstraint.targetAltitude + VNavUtils.altitudeForDistance(followingConstraint.fpa, followingConstraint.distance);
                                if ((followingConstraint.fpa > 0 && constraintAltForDist <= constraint.targetAltitude + 25) || constraint.fpa === 0) {
                                    constraintIsBod = false;
                                }
                            }
                        }
                        if (constraint.index === verticalPlan.lastDescentConstraintLegIndex) {
                            constraint.isPathEnd = true;
                            constraint.isTarget = true;
                            constraintIsBod = true;
                        }
                        for (let legIndex = 0; legIndex < constraint.legs.length; legIndex++) {
                            const leg = constraint.legs[legIndex];
                            leg.fpa = constraint.fpa;
                            leg.altitude = altitude;
                            altitude += VNavUtils.altitudeForDistance(leg.fpa, leg.distance);
                            if (legIndex === 0) {
                                leg.isAdvisory = false;
                            }
                            else {
                                leg.isAdvisory = true;
                            }
                            if (legIndex === 0 && constraint.isTarget && constraintIsBod) {
                                leg.isBod = true;
                            }
                            else {
                                leg.isBod = false;
                            }
                        }
                    }
                }
            }
        }
        /**
         * Fills the VNAV plan leg and constraint segment distances.
         * @param lateralPlan The Lateral Flight Plan.
         * @param verticalPlan The Vertical Flight Plan.
         */
        fillLegDistances(lateralPlan, verticalPlan) {
            var _a, _b, _c;
            if (lateralPlan.length > 0) {
                for (const segment of lateralPlan.segments()) {
                    if (segment) {
                        const vnavSegment = verticalPlan.segments[segment.segmentIndex];
                        for (let l = 0; l < segment.legs.length; l++) {
                            const leg = segment.legs[l];
                            if (leg && leg.calculated && leg.calculated.distanceWithTransitions) {
                                vnavSegment.legs[l].distance = leg.calculated.distanceWithTransitions;
                            }
                            else if (leg && leg.calculated && leg.calculated.endLat !== undefined && leg.calculated.endLon !== undefined) {
                                let prevLeg;
                                for (const checkLeg of lateralPlan.legs(true, segment.offset + l - 1)) {
                                    if (((_a = checkLeg.calculated) === null || _a === void 0 ? void 0 : _a.endLat) !== undefined && ((_b = checkLeg.calculated) === null || _b === void 0 ? void 0 : _b.endLon) !== undefined) {
                                        prevLeg = checkLeg;
                                        break;
                                    }
                                }
                                if (((_c = prevLeg === null || prevLeg === void 0 ? void 0 : prevLeg.calculated) === null || _c === void 0 ? void 0 : _c.endLat) && prevLeg.calculated.endLon) {
                                    vnavSegment.legs[l].distance = UnitType.GA_RADIAN.convertTo(GeoPoint.distance(leg.calculated.endLat, leg.calculated.endLon, prevLeg.calculated.endLat, prevLeg.calculated.endLon), UnitType.METER);
                                }
                            }
                            else {
                                vnavSegment.legs[l].distance = 0;
                            }
                        }
                    }
                }
            }
        }
        /**
         * Finds and removes invalid constraints from the vertical plan.
         * @param verticalPlan The Vertical Flight Plan.
         */
        findAndRemoveInvalidConstraints(verticalPlan) {
            var _a, _b, _c;
            let firstDescentConstraintIndex = verticalPlan.firstDescentConstraintLegIndex === undefined
                ? -1
                : VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, verticalPlan.firstDescentConstraintLegIndex);
            // If there is a vertical direct-to active (and it has not been invalidated), skip all constraints prior to the
            // direct-to.
            const startIndex = ((_a = verticalPlan.constraints[firstDescentConstraintIndex]) === null || _a === void 0 ? void 0 : _a.type) === 'direct'
                ? firstDescentConstraintIndex
                : verticalPlan.constraints.length - 1;
            let phase = 'climb';
            let priorMinAltitude = -Infinity;
            let priorMaxAltitude = Infinity;
            let distanceFromPriorMinAltitude = 0;
            let requiredFpa = 0;
            for (let i = startIndex; i >= 0; i--) {
                const currentConstraint = verticalPlan.constraints[i];
                const currentConstraintDistance = VNavUtils.getConstraintDistanceFromLegs(currentConstraint, verticalPlan.constraints[i + 1], verticalPlan);
                let currentPhase;
                switch (currentConstraint.type) {
                    case 'climb':
                    case 'missed':
                        currentPhase = currentConstraint.type;
                        break;
                    default:
                        currentPhase = 'descent';
                }
                if (currentPhase !== phase) {
                    // Reset prior altitudes when switching phases.
                    phase = currentPhase;
                    priorMinAltitude = -Infinity;
                    priorMaxAltitude = Infinity;
                    distanceFromPriorMinAltitude = currentConstraintDistance;
                }
                else {
                    distanceFromPriorMinAltitude += currentConstraintDistance;
                }
                let isDescentConstraint;
                let shouldInvalidate;
                switch (phase) {
                    case 'climb':
                    case 'missed':
                        isDescentConstraint = false;
                        shouldInvalidate = this.invalidateClimbConstraintFunc(currentConstraint, i, verticalPlan.constraints, firstDescentConstraintIndex, priorMinAltitude, priorMaxAltitude);
                        break;
                    default:
                        isDescentConstraint = true;
                        if (isFinite(priorMinAltitude) && isFinite(currentConstraint.maxAltitude)) {
                            requiredFpa = Math.max(0, -VNavUtils.getFpa(distanceFromPriorMinAltitude, currentConstraint.maxAltitude - priorMinAltitude));
                        }
                        else {
                            requiredFpa = 0;
                        }
                        shouldInvalidate = this.invalidateDescentConstraintFunc(currentConstraint, i, verticalPlan.constraints, priorMinAltitude, priorMaxAltitude, requiredFpa, this.maxFlightPathAngle);
                }
                const constraintLeg = VNavUtils.getVerticalLegFromPlan(verticalPlan, currentConstraint.index);
                if (shouldInvalidate) {
                    constraintLeg.invalidConstraintAltitude = currentConstraint.minAltitude !== Number.NEGATIVE_INFINITY ? currentConstraint.minAltitude : currentConstraint.maxAltitude;
                    verticalPlan.constraints.splice(i, 1);
                    // Need to subtract current constraint distance because it will get added again at the beginning of the next iteration.
                    // (The next constraint inherits the legs that belonged to the current constraint after it is removed.)
                    distanceFromPriorMinAltitude -= currentConstraintDistance;
                    // If we invalidated the first descent constraint, we need to find the new one.
                    if (isDescentConstraint && i === firstDescentConstraintIndex) {
                        firstDescentConstraintIndex = VNavUtils.getFirstDescentConstraintIndex(verticalPlan);
                        verticalPlan.firstDescentConstraintLegIndex = (_b = verticalPlan.constraints[firstDescentConstraintIndex]) === null || _b === void 0 ? void 0 : _b.index;
                    }
                }
                else {
                    constraintLeg.invalidConstraintAltitude = undefined;
                    if (isFinite(currentConstraint.minAltitude)) {
                        priorMinAltitude = currentConstraint.minAltitude;
                        distanceFromPriorMinAltitude = 0;
                    }
                    if (isFinite(currentConstraint.maxAltitude)) {
                        priorMaxAltitude = currentConstraint.maxAltitude;
                    }
                }
            }
            // Update last descent leg in case we invalidated some descent constraints
            verticalPlan.lastDescentConstraintLegIndex = (_c = verticalPlan.constraints[VNavUtils.getLastDescentConstraintIndex(verticalPlan)]) === null || _c === void 0 ? void 0 : _c.index;
        }
        /**
         * Finds previously invalidated constraints and re-inserts them into the vertical flight plan.
         * @param verticalPlan The Vertical Flight Plan.
         * @param lateralPlan The Lateral Flight Plan.
         */
        reinsertInvalidConstraints(verticalPlan, lateralPlan) {
            var _a;
            const firstDescentConstraintIndex = verticalPlan.firstDescentConstraintLegIndex === undefined
                ? -1
                : VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, verticalPlan.firstDescentConstraintLegIndex);
            // If there is a vertical direct-to active (and it has not been invalidated), skip all legs prior to and including
            // the direct-to.
            const startIndex = ((_a = verticalPlan.constraints[firstDescentConstraintIndex]) === null || _a === void 0 ? void 0 : _a.type) === 'direct'
                ? (verticalPlan.firstDescentConstraintLegIndex + 1)
                : 0;
            let globalLegIndex = startIndex;
            for (const lateralLeg of lateralPlan.legs(false, startIndex)) {
                const verticalLeg = VNavUtils.getVerticalLegFromPlan(verticalPlan, globalLegIndex);
                if (verticalLeg.invalidConstraintAltitude !== undefined) {
                    const constraintIndex = VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, globalLegIndex);
                    const constraintAltitudes = SmoothingPathCalculator.getConstraintAltitudes(lateralLeg, this.legAltitudes);
                    if (constraintAltitudes !== undefined) {
                        const proposedConstraint = this.buildConstraint(verticalPlan, globalLegIndex, lateralLeg, constraintAltitudes, verticalLeg.name);
                        verticalPlan.constraints.splice(constraintIndex + 1, 0, proposedConstraint);
                        // If we re-validated a descent constraint, we need to update the first/last descent constraint when appropriate.
                        if (proposedConstraint.type === 'descent'
                            || proposedConstraint.type === 'manual'
                            || proposedConstraint.type === 'direct'
                            || proposedConstraint.type === 'dest') {
                            if (verticalPlan.firstDescentConstraintLegIndex === undefined || globalLegIndex < verticalPlan.firstDescentConstraintLegIndex) {
                                verticalPlan.firstDescentConstraintLegIndex = globalLegIndex;
                            }
                            if (verticalPlan.lastDescentConstraintLegIndex === undefined || globalLegIndex > verticalPlan.lastDescentConstraintLegIndex) {
                                verticalPlan.lastDescentConstraintLegIndex = globalLegIndex;
                            }
                        }
                    }
                }
                globalLegIndex++;
            }
        }
        /**
         * Populates a vertical flight plan's constraints with legs and updates the constraint distances and VNAV path
         * eligibility data.
         * @param verticalPlan The vertical flight plan for which to populate constraints.
         */
        populateConstraints(verticalPlan) {
            for (let constraintIndex = 0; constraintIndex < verticalPlan.constraints.length; constraintIndex++) {
                const constraint = verticalPlan.constraints[constraintIndex];
                const previousConstraint = verticalPlan.constraints[constraintIndex + 1];
                constraint.legs.length = 0;
                constraint.distance = VNavUtils.getConstraintDistanceFromLegs(constraint, previousConstraint, verticalPlan);
                let eligibleLegIndex = constraint.index + 1;
                let ineligibleLegIndex;
                for (let globalLegIndex = constraint.index; globalLegIndex > (previousConstraint !== undefined ? previousConstraint.index : -1); globalLegIndex--) {
                    const verticalLeg = VNavUtils.getVerticalLegFromPlan(verticalPlan, globalLegIndex);
                    constraint.legs.push(verticalLeg);
                    if (ineligibleLegIndex === undefined && verticalLeg.isEligible) {
                        eligibleLegIndex = globalLegIndex;
                    }
                    if (ineligibleLegIndex === undefined && !verticalLeg.isEligible) {
                        ineligibleLegIndex = globalLegIndex;
                    }
                }
                if (ineligibleLegIndex !== undefined) {
                    constraint.nextVnavEligibleLegIndex = eligibleLegIndex;
                }
            }
        }
        /**
         * Computes the flight path angles for each constraint segment.
         * @param verticalPlan The Vertical Flight Plan.
         * @returns Whether the flight path angles were computed.
         */
        computeFlightPathAngles(verticalPlan) {
            // Iterate through all descent constraints in reverse flight plan order and attempt to assign one as a "target"
            // constraint, which is a constraint that anchors a constant FPA path connecting it to one or more prior
            // constraints.
            // Once a target constraint is found, the iteration continues as we attempt to build a constant FPA path backwards
            // from the target constraint that meets all the iterated constraints. Once we reach a constraint that cannot be
            // met with a constant FPA path from the target constraint that also meets all intermediate constraints, we assign
            // a new target constraint at the point where the FPA must change. Certain constraints must also be designated as
            // target constraints regardless of whether a constant FPA path through them is possible. In any case, once we
            // designate a new target constraint, the process is repeated until we run out of descent constraints.
            let currentTargetConstraint;
            let currentPathSegmentDistance = 0;
            let currentPathSegmentMinFpa = this.minFlightPathAngle;
            let currentPathSegmentMaxFpa = this.maxFlightPathAngle;
            let currentTargetConstraintHasFixedFpa = false;
            const firstDescentConstraintIndex = verticalPlan.firstDescentConstraintLegIndex === undefined
                ? -1
                : VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, verticalPlan.firstDescentConstraintLegIndex);
            const lastDescentConstraintIndex = verticalPlan.lastDescentConstraintLegIndex === undefined
                ? -1
                : VNavUtils.getConstraintIndexFromLegIndex(verticalPlan, verticalPlan.lastDescentConstraintLegIndex);
            if (firstDescentConstraintIndex < 0 || lastDescentConstraintIndex < 0) {
                // There are no descent constraints, so no FPAs to be calculated
                return false;
            }
            for (let targetConstraintIndex = lastDescentConstraintIndex; targetConstraintIndex <= firstDescentConstraintIndex; targetConstraintIndex++) {
                const constraint = verticalPlan.constraints[targetConstraintIndex];
                // If the current constraint is climb or missed, skip it.
                if (constraint.type === 'climb' || constraint.type === 'missed') {
                    continue;
                }
                // If we haven't found a target constraint yet, attempt to make the current constraint the target constraint,
                // if it defines either a minimum or maximum altitude. The target altitude is preferentially set to the minimum
                // altitude, if it exists. If the current constraint has neither a minimum nor maximum altitude (which should
                // technically never happen), skip it.
                if (!currentTargetConstraint) {
                    if (constraint.minAltitude > Number.NEGATIVE_INFINITY || constraint.maxAltitude < Number.POSITIVE_INFINITY) {
                        currentTargetConstraint = constraint;
                        currentTargetConstraint.targetAltitude = constraint.minAltitude > Number.NEGATIVE_INFINITY ? constraint.minAltitude : constraint.maxAltitude;
                        currentTargetConstraint.isTarget = true;
                    }
                    else {
                        continue;
                    }
                }
                // Reset the method variables
                currentPathSegmentMinFpa = this.minFlightPathAngle;
                currentPathSegmentMaxFpa = this.maxFlightPathAngle;
                currentPathSegmentDistance = currentTargetConstraint.distance;
                const currentTargetConstraintIsFirstDescentConstraint = targetConstraintIndex === firstDescentConstraintIndex;
                if (currentTargetConstraintIsFirstDescentConstraint) {
                    if (currentTargetConstraint.type === 'descent') {
                        // If this is the first descent constraint and it is not a direct or manual, set the FPA to the default value.
                        currentTargetConstraint.fpa = this.flightPathAngle;
                    }
                    // If currentTargetConstraintIsFirstDescentConstraint is true, then after this logic, we're done with this method.
                    return true;
                }
                // If the current target constraint is a manual or direct type, then honor the FPA by not allowing any other FPAs.
                if (currentTargetConstraint.type === 'manual') {
                    currentPathSegmentMinFpa = currentTargetConstraint.fpa;
                    currentPathSegmentMaxFpa = currentTargetConstraint.fpa;
                    currentTargetConstraintHasFixedFpa = true;
                }
                else {
                    currentTargetConstraintHasFixedFpa = false;
                }
                let pathSegmentIsFlat = false;
                let ineligibleFollowingConstraint = currentTargetConstraint.nextVnavEligibleLegIndex !== undefined
                    ? currentTargetConstraint
                    : undefined;
                for (let currentConstraintIndex = targetConstraintIndex + 1; currentConstraintIndex <= firstDescentConstraintIndex; currentConstraintIndex++) {
                    const currentConstraint = verticalPlan.constraints[currentConstraintIndex];
                    const isCurrentConstraintFirstDescent = currentConstraintIndex === firstDescentConstraintIndex;
                    const isCurrentConstraintFaf = currentConstraint.index === verticalPlan.fafLegIndex;
                    const isCurrentConstraintClimb = currentConstraint.type === 'climb' || currentConstraint.type === 'missed';
                    const isCurrentConstraintManual = currentConstraint.type === 'manual';
                    const isCurrentConstraintDirect = currentConstraint.type === 'direct';
                    if (isCurrentConstraintClimb) {
                        // We have reached a climb constraint.
                        if (currentConstraintIndex - 1 > targetConstraintIndex) {
                            // There is at least one constraint between the existing target constraint and the current climb
                            // constraint. Attempt to extend the constant-FPA path through the constraint immediately following the
                            // current climb constraint (which is guaranteed to be a descent constraint).
                            currentTargetConstraint.fpa = MathUtils.clamp(this.flightPathAngle, currentPathSegmentMinFpa, currentPathSegmentMaxFpa);
                            const maxAltitude = pathSegmentIsFlat ? currentTargetConstraint.targetAltitude : verticalPlan.constraints[currentConstraintIndex - 1].maxAltitude;
                            const terminatedIndex = this.terminateSmoothedPath(verticalPlan, targetConstraintIndex, currentConstraintIndex, maxAltitude, false);
                            if (terminatedIndex < currentConstraintIndex) {
                                // The path was terminated early, which means there is a new target constraint.
                                targetConstraintIndex = terminatedIndex - 1; // reduce the targetConstraintIndex by 1 because the for loop will +1 it.
                                currentTargetConstraint = verticalPlan.constraints[terminatedIndex];
                                break;
                            }
                        }
                        else {
                            // The existing target constraint immediately follows the current climb constraint. Treat the target
                            // constraint as if it were the first descent constraint and apply the default FPA. Note that we are
                            // guaranteed the target constraint is not a direct constraint.
                            currentTargetConstraint.fpa = this.flightPathAngle;
                        }
                        // Do not designate a new target constraint in order to allow the outer loop to find the new one.
                        targetConstraintIndex = currentConstraintIndex;
                        currentTargetConstraint = undefined;
                        break;
                    }
                    if (ineligibleFollowingConstraint) {
                        // The constraint following the current one (in flight plan order) is path-ineligible. In this case, we will
                        // attempt to make the current constraint the new target constraint since we cannot compute a valid path from
                        // the current constraint to the target constraint.
                        // Because the following constraint is path-ineligible, the FPA of the target constraint does not have to be
                        // restricted by the current constraint, since the path between the two is undefined.
                        currentTargetConstraint.fpa = MathUtils.clamp(this.flightPathAngle, currentPathSegmentMinFpa, currentPathSegmentMaxFpa);
                        // Attempt to set the maximum altitude of the path from the current constraint to the current target
                        // constraint to the minimum altitude of the current constraint, if it exists, or to the maximum altitude
                        // otherwise. We will then clamp this from below using the current target constraint's target altitude. Since
                        // we didn't restrict the current target constraint's FPA based on the current constraint, this ensures we
                        // don't have to climb when traveling from the current constraint to the current target constraint. We will
                        // also clamp from above using the prior (in flight plan order) maximum altitude constraint. This ensures we
                        // don't have to descend when traveling to the current constraint.
                        const currentConstraintTargetAltitude = currentConstraint.minAltitude > Number.NEGATIVE_INFINITY
                            ? currentConstraint.minAltitude
                            : currentConstraint.maxAltitude;
                        const priorMaxAltitude = SmoothingPathCalculator.findPriorMaxAltitude(verticalPlan, currentConstraintIndex, firstDescentConstraintIndex);
                        const maxAltitude = Math.min(Math.max(currentConstraintTargetAltitude, currentTargetConstraint.targetAltitude), priorMaxAltitude);
                        const terminatedIndex = this.terminateSmoothedPath(verticalPlan, targetConstraintIndex, currentConstraintIndex, maxAltitude, true);
                        if (terminatedIndex < currentConstraintIndex) {
                            // The path was terminated early, which means there is a new target constraint.
                            targetConstraintIndex = terminatedIndex - 1; // reduce the targetConstraintIndex by 1 because the for loop will +1 it.
                            currentTargetConstraint = verticalPlan.constraints[terminatedIndex];
                            break;
                        }
                        else {
                            targetConstraintIndex = currentConstraintIndex - 1; // reduce the targetConstraintIndex by 1 because the for loop will +1 it.
                            currentTargetConstraint = currentConstraint;
                        }
                        break;
                    }
                    const minAltitude = currentConstraint.minAltitude;
                    const maxAltitude = currentConstraint.maxAltitude;
                    if (pathSegmentIsFlat && maxAltitude - currentTargetConstraint.targetAltitude > 0) {
                        // We are in a flat segment (all constraints with FPA = 0) and the current constraint would allow a
                        // non-zero FPA to the constraint immediately following it. Therefore, we set the new target constraint
                        // to the constraint immediately following the current one (because it is at the end of that constraint
                        // where the FPA can potentially change from non-zero to zero). Note that we are guaranteed that the
                        // new target constraint lies prior to the existing target constraint.
                        const flatSegmentAltitude = currentTargetConstraint.targetAltitude;
                        const newTargetConstraintIndex = currentConstraintIndex - 1;
                        SmoothingPathCalculator.applyPathValuesToSmoothedConstraints(verticalPlan, targetConstraintIndex, newTargetConstraintIndex, 
                        // Maximum altitude is not needed because we are guaranteed that the target altitudes of all smoothed
                        // constraints are equal to the flat segment altitude.
                        Infinity, this.applyPathValuesResult);
                        // reduce the targetConstraintIndex by 1 because the for loop will +1 it.
                        targetConstraintIndex = newTargetConstraintIndex - 1;
                        currentTargetConstraint = verticalPlan.constraints[newTargetConstraintIndex];
                        currentTargetConstraint.targetAltitude = flatSegmentAltitude;
                        currentTargetConstraint.isTarget = true;
                        break;
                    }
                    else if (!currentTargetConstraintHasFixedFpa && maxAltitude - currentTargetConstraint.targetAltitude <= 0) {
                        // The current constraint does not allow a non-zero FPA to the target constraint, and the target constraint
                        // does not have a fixed FPA. We will mark the current segment as flat and set the target constraint FPA to 0.
                        pathSegmentIsFlat = true;
                        currentTargetConstraint.fpa = 0;
                        if (isCurrentConstraintFirstDescent) {
                            // If the current constraint is the first descent constraint, then we need to make it the new target
                            // constraint because the first descent constraint is never flat.
                            const flatSegmentAltitude = currentTargetConstraint.targetAltitude;
                            SmoothingPathCalculator.applyPathValuesToSmoothedConstraints(verticalPlan, targetConstraintIndex, currentConstraintIndex, 
                            // Maximum altitude is not needed because we are guaranteed that the target altitudes of all smoothed
                            // constraints are equal to the flat segment altitude.
                            Infinity, this.applyPathValuesResult);
                            // reduce the targetConstraintIndex by 1 because the for loop will +1 it.
                            targetConstraintIndex = currentConstraintIndex - 1;
                            currentTargetConstraint = verticalPlan.constraints[currentConstraintIndex];
                            currentTargetConstraint.targetAltitude = flatSegmentAltitude;
                            currentTargetConstraint.isTarget = true;
                            break;
                        }
                        continue;
                    }
                    // Get the min and max FPA from the current target constraint to the current constraint.
                    const minFpa = VNavUtils.getFpa(currentPathSegmentDistance, minAltitude - currentTargetConstraint.targetAltitude);
                    const maxFpa = VNavUtils.getFpa(currentPathSegmentDistance, maxAltitude - currentTargetConstraint.targetAltitude);
                    const isFpaOutOfBounds = minFpa > currentPathSegmentMaxFpa || maxFpa < currentPathSegmentMinFpa;
                    // A new target constraint needs to be created under the following conditions:
                    // - The current constraint cannot be met with a constant FPA path from the current target constraint within
                    //   this calculator's FPA limits.
                    // - The current constraint is the final approach fix.
                    // - The current constraint is a vertical direct constraint.
                    // - The current constraint is a manual constraint.
                    if (isFpaOutOfBounds || isCurrentConstraintFaf || isCurrentConstraintManual || isCurrentConstraintDirect) {
                        // We need to choose a FPA for the constant-FPA smoothed path.
                        if (isFpaOutOfBounds) {
                            // If we are creating a new target constraint because the current constraint can't be met with a
                            // constant-FPA path, then we set the FPA of the smoothed path to the value that brings the new
                            // target constraint's target altitude as close to meeting the current constraint as possible.
                            if (minFpa > currentPathSegmentMaxFpa) {
                                currentTargetConstraint.fpa = currentPathSegmentMaxFpa;
                            }
                            else {
                                currentTargetConstraint.fpa = currentPathSegmentMinFpa;
                            }
                        }
                        else {
                            // If the new target constraint can be met with a constant-FPA path, then we choose a valid FPA that is
                            // as close to the calculator's default FPA as possible.
                            currentPathSegmentMinFpa = Math.max(minFpa, currentPathSegmentMinFpa);
                            currentPathSegmentMaxFpa = Math.min(maxFpa, currentPathSegmentMaxFpa);
                            currentTargetConstraint.fpa = MathUtils.clamp(this.flightPathAngle, currentPathSegmentMinFpa, currentPathSegmentMaxFpa);
                        }
                        // Set the maximum altitude of the path from the current constraint to the current target constraint to the
                        // prior (in flight plan order) maximum altitude constraint. This ensures we don't have to descend when
                        // traveling to the current constraint.
                        const priorMaxAltitude = SmoothingPathCalculator.findPriorMaxAltitude(verticalPlan, currentConstraintIndex, firstDescentConstraintIndex);
                        // Attempt to extend a constant-FPA path from the existing target constraint to the current constraint and
                        // make the current constraint the new target constraint.
                        const terminatedIndex = this.terminateSmoothedPath(verticalPlan, targetConstraintIndex, currentConstraintIndex, priorMaxAltitude, true);
                        targetConstraintIndex = terminatedIndex - 1; // reduce the nextTargetConstraintIndex by 1 because the for loop will +1 it.
                        currentTargetConstraint = verticalPlan.constraints[terminatedIndex];
                        break;
                    }
                    else if (isCurrentConstraintFirstDescent) {
                        // We have reached the first descent constraint without needing to create a new target constraint, so
                        // attempt to extend the constant-FPA path from the existing target constraint through the first descent
                        // constraint.
                        currentPathSegmentMinFpa = Math.max(minFpa, currentPathSegmentMinFpa);
                        currentPathSegmentMaxFpa = Math.min(maxFpa, currentPathSegmentMaxFpa);
                        currentTargetConstraint.fpa = MathUtils.clamp(this.flightPathAngle, currentPathSegmentMinFpa, currentPathSegmentMaxFpa);
                        const terminatedIndex = this.terminateSmoothedPath(verticalPlan, targetConstraintIndex, currentConstraintIndex + 1, currentConstraint.maxAltitude, false);
                        if (terminatedIndex < currentConstraintIndex + 1) {
                            // The path was terminated early, which means there is a new target constraint.
                            targetConstraintIndex = terminatedIndex - 1; // reduce the nextTargetConstraintIndex by 1 because the for loop will +1 it.
                            currentTargetConstraint = verticalPlan.constraints[terminatedIndex];
                            break;
                        }
                        else {
                            // The path was not terminated early, so we are done.
                            return true;
                        }
                    }
                    else {
                        // Extend the current constant-FPA path and update the FPA limits
                        currentPathSegmentMinFpa = Math.max(minFpa, currentPathSegmentMinFpa);
                        currentPathSegmentMaxFpa = Math.min(maxFpa, currentPathSegmentMaxFpa);
                        currentPathSegmentDistance += currentConstraint.distance;
                    }
                    ineligibleFollowingConstraint = currentConstraint.nextVnavEligibleLegIndex !== undefined
                        ? currentConstraint
                        : undefined;
                }
            }
            return true;
        }
        /**
         * Attempts to extend and terminate a constant-FPA path from an existing target constraint at another constraint,
         * applying flight path angles and target altitudes to each constraint along the path. The target constraint defines
         * the FPA of the path.
         *
         * If the target altitude of one of the constraints in the sequence, as prescribed by the path, violates a maximum
         * altitude, the path will be terminated at the constraint immediately following (in flight plan order) the violating
         * constraint, and FPA and target altitudes will not be written to the terminating constraint or any prior
         * constraints.
         * @param verticalPlan The vertical flight plan.
         * @param targetConstraintIndex The index of the target constraint.
         * @param terminatingConstraintIndex The index of the constraint at which to terminate the path.
         * @param maxAltitude The maximum allowable target altitude, in meters.
         * @param terminatingConstraintIsTarget Whether to designate the terminating constraint as a target constraint if the
         * path is not terminated early. If the path is terminated early, this argument is ignored and the constraint at
         * which the path was terminated early is always designated as a target constraint.
         * @returns The index of the constraint at which the constant-FPA path was actually terminated.
         */
        terminateSmoothedPath(verticalPlan, targetConstraintIndex, terminatingConstraintIndex, maxAltitude, terminatingConstraintIsTarget) {
            const [maxAltitudeViolatedIndex, smoothedSegmentDistance] = SmoothingPathCalculator.applyPathValuesToSmoothedConstraints(verticalPlan, targetConstraintIndex, terminatingConstraintIndex, maxAltitude, this.applyPathValuesResult);
            if (terminatingConstraintIsTarget || maxAltitudeViolatedIndex !== undefined) {
                // A constant-FPA path was not able to be extended from the existing target constraint to the first descent
                // constraint, so we need to designate a new target constraint where the path terminated.
                const currentTargetConstraint = verticalPlan.constraints[targetConstraintIndex];
                // Establish the proposed next target constraint target altitude
                const proposedNewTargetConstraintAltitude = currentTargetConstraint.targetAltitude + VNavUtils.altitudeForDistance(currentTargetConstraint.fpa, smoothedSegmentDistance);
                const newTargetConstraintIndex = maxAltitudeViolatedIndex !== null && maxAltitudeViolatedIndex !== void 0 ? maxAltitudeViolatedIndex : terminatingConstraintIndex;
                // Set the new target constraint values
                const newTargetConstraint = verticalPlan.constraints[newTargetConstraintIndex];
                newTargetConstraint.isTarget = true;
                newTargetConstraint.targetAltitude = MathUtils.clamp(proposedNewTargetConstraintAltitude, newTargetConstraint.minAltitude, Math.min(newTargetConstraint.maxAltitude, maxAltitude));
            }
            return maxAltitudeViolatedIndex !== null && maxAltitudeViolatedIndex !== void 0 ? maxAltitudeViolatedIndex : terminatingConstraintIndex;
        }
        /** @inheritdoc */
        getFirstDescentConstraintAltitude(planIndex) {
            const verticalPlan = this.getVerticalFlightPlan(planIndex);
            if (verticalPlan.constraints.length > 0) {
                for (let i = verticalPlan.constraints.length - 1; i >= 0; i--) {
                    const constraint = verticalPlan.constraints[i];
                    if (constraint.type !== 'climb') {
                        return constraint.targetAltitude;
                    }
                }
            }
            return undefined;
        }
        // Start of buildVerticalFlightPlan helper methods
        /**
         * Gets the constraint altitudes for a lateral flight plan leg.
         * @param leg A lateral flight plan leg.
         * @param out The tuple to which to write the altitudes, as `[minimum_altitude, maximum_altitude]`.
         * @returns The constraint altitudes, in meters, for the specified flight plan leg, as
         * `[minimum_altitude, maximum_altitude]`, or `undefined` if the leg does not define any altitude constraints.
         */
        static getConstraintAltitudes(leg, out) {
            if (leg.verticalData !== undefined) {
                switch (leg.verticalData.altDesc) {
                    case exports.AltitudeRestrictionType.At:
                        out[0] = leg.verticalData.altitude1;
                        out[1] = leg.verticalData.altitude1;
                        return out;
                    case exports.AltitudeRestrictionType.AtOrAbove:
                        out[0] = leg.verticalData.altitude1;
                        out[1] = Number.POSITIVE_INFINITY;
                        return out;
                    case exports.AltitudeRestrictionType.AtOrBelow:
                        out[0] = Number.NEGATIVE_INFINITY;
                        out[1] = leg.verticalData.altitude1;
                        return out;
                    case exports.AltitudeRestrictionType.Between:
                        out[0] = leg.verticalData.altitude2;
                        out[1] = leg.verticalData.altitude1;
                        return out;
                }
            }
            return undefined;
        }
        /**
         * Forces a constraint to an AT constraint.
         * @param constraint The constraint to force to an AT constraint.
         */
        static forceAtConstraint(constraint) {
            if (constraint.minAltitude !== constraint.maxAltitude) {
                if (constraint.minAltitude > Number.NEGATIVE_INFINITY) {
                    constraint.maxAltitude = constraint.minAltitude;
                }
                else {
                    constraint.minAltitude = constraint.maxAltitude;
                }
            }
        }
        /**
         * Gets the global index of a flight plan's lateral direct-to target leg.
         * @param lateralPlan A flight plan.
         * @returns The global index of the flight plan's lateral direct-to target leg, or `undefined` if the plan does not
         * have an existing lateral direct-to.
         */
        static getDirectToTargetLegIndex(lateralPlan) {
            const directToData = lateralPlan.directToData;
            if (lateralPlan.length > 0 && directToData.segmentIndex > -1 && directToData.segmentLegIndex > -1) {
                const segment = lateralPlan.tryGetSegment(directToData.segmentIndex);
                if (segment !== null) {
                    return segment.offset + directToData.segmentLegIndex;
                }
            }
            return undefined;
        }
        /**
         * Checks if there is a lateral direct-to leg in the flight plan and if so, flags the corresponding vertical flight
         * plan leg as such and marks the first descent constraint
         * @param lateralPlan The Lateral Flight Plan.
         * @param verticalPlan The Vertical Flight Plan.
         * @param directToLegOffset The offset of the lateral direct-to leg from the direct-to target leg.
         */
        static handleDirectToLegInVerticalPlan(lateralPlan, verticalPlan, directToLegOffset) {
            // Check for a direct to in the lateral plan
            if (lateralPlan.directToData.segmentIndex > -1 && lateralPlan.directToData.segmentLegIndex > -1) {
                const directLateralLeg = lateralPlan.getLeg(lateralPlan.directToData.segmentIndex, lateralPlan.directToData.segmentLegIndex + directToLegOffset);
                if (BitFlags.isAll(directLateralLeg.flags, exports.LegDefinitionFlags.DirectTo)) {
                    const directVerticalLeg = VNavUtils.getVerticalLegFromSegmentInPlan(verticalPlan, lateralPlan.directToData.segmentIndex, lateralPlan.directToData.segmentLegIndex + directToLegOffset);
                    directVerticalLeg.isDirectToTarget = true;
                    const segment = verticalPlan.segments[lateralPlan.directToData.segmentIndex];
                    if (segment !== undefined) {
                        const globalLegIndex = segment.offset + lateralPlan.directToData.segmentLegIndex + directToLegOffset;
                        for (let i = verticalPlan.constraints.length - 1; i >= 0; i--) {
                            const constraint = verticalPlan.constraints[i];
                            if (constraint.type !== 'climb' && constraint.type !== 'missed' && constraint.index >= globalLegIndex) {
                                verticalPlan.firstDescentConstraintLegIndex = constraint.index;
                                return;
                            }
                        }
                        verticalPlan.firstDescentConstraintLegIndex = undefined;
                    }
                }
            }
        }
        /**
         * Checks whether a leg constraint is part of the missed approach.
         * @param lateralSegment The lateral flight plan segment to which the constraint's leg belongs.
         * @param lateralLeg The lateral flight plan leg to which the constraint belongs.
         * @returns Whether the leg constraint is part of the missed approach.
         */
        static isConstraintInMissedApproach(lateralSegment, lateralLeg) {
            if (lateralSegment.segmentType === exports.FlightPlanSegmentType.Approach && BitFlags.isAny(lateralLeg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                return true;
            }
            return false;
        }
        /**
         * Checks whether a leg constriant is a descent constraint and is higher than the prior descent leg constraint.
         * @param previousConstrant The previous VNav Constraint.
         * @param currentConstraint The current VNav Constraint.
         * @returns Whether the current constraint is higher than the previous constraint.
         */
        static isConstraintHigherThanPriorConstraint(previousConstrant, currentConstraint) {
            const currentMinWithPrecision = Math.round(currentConstraint.minAltitude * 10) / 10;
            const priorMaxWithPrecision = Math.round(previousConstrant.maxAltitude * 10) / 10;
            if (currentMinWithPrecision > priorMaxWithPrecision) {
                return true;
            }
            return false;
        }
        /**
         * Checks whether a leg constraint requires an FPA greater than the max allowed value.
         * @param previousConstrant The previous VNavConstraint.
         * @param currentConstraint The VNavConstraint being evaluated.
         * @param verticalPlan The vertical flight plan.
         * @param maxFpa The maximum FPA allowed.
         * @returns Whether this constraint requires an invalid FPA.
         */
        static doesConstraintRequireInvalidFpa(previousConstrant, currentConstraint, verticalPlan, maxFpa) {
            if (currentConstraint.maxAltitude < Number.POSITIVE_INFINITY && previousConstrant.minAltitude >= 0) {
                const constraintDistance = VNavUtils.getConstraintDistanceFromLegs(currentConstraint, previousConstrant, verticalPlan);
                const minFpaTempValue = VNavUtils.getFpa(constraintDistance, Math.abs(currentConstraint.maxAltitude - previousConstrant.minAltitude));
                if (minFpaTempValue > maxFpa) {
                    return true;
                }
            }
            return false;
        }
        /**
         * The default function which checks whether a lateral flight plan leg is eligible for VNAV.
         * @param lateralLeg A lateral flight plan leg.
         * @returns Whether the specified leg is eligible for VNAV.
         */
        static isLegVnavEligible(lateralLeg) {
            switch (lateralLeg.leg.type) {
                case exports.LegType.VM:
                case exports.LegType.FM:
                case exports.LegType.Discontinuity:
                case exports.LegType.ThruDiscontinuity:
                    return false;
                default:
                    return true;
            }
        }
        /**
         * The default function which checks whether a climb constraint should be invalidated. This function always returns
         * `false`.
         * @returns Whether the specified climb constraint should be invalidated (always `false`).
         */
        static invalidateClimbConstraint() {
            return false;
        }
        /**
         * The default function which checks whether a descent constraint should be invalidated.
         * @param constraint A descent constraint.
         * @param index The index of the constraint to check.
         * @param constraints The array of VNAV constraints currently in the vertical flight plan.
         * @param priorMinAltitude The most recent minimum altitude, in meters, defined by a VNAV constraint prior to the
         * constraint to check. Only prior constraints connected to the constraint to check by a contiguous sequence of
         * descent constraints are included.
         * @param priorMaxAltitude The most recent maximum altitude, in meters, defined by a VNAV constraint prior to the
         * constraint to check. Only prior constraints connected to the constraint to check by a contiguous sequence of
         * descent constraints are included.
         * @param requiredFpa The minimum flight path angle, in degrees, required to meet the maximum altitude of the
         * constraint to check, assuming a descent starting from the constraint defining the most recent prior minimum
         * altitude. Positive values indicate a descending path. If there is no required FPA because there is no defined
         * prior minimum altitude or maximum altitude for the constraint to check, or if the constraint to check is higher
         * than the prior minimum altitude, then this value will equal zero.
         * @param maxFpa The maximum allowed flight path angle, in degrees. Positive values indicate a descending path.
         * @returns Whether the specified descent constraint should be invalidated.
         */
        static invalidateDescentConstraint(constraint, index, constraints, priorMinAltitude, priorMaxAltitude, requiredFpa, maxFpa) {
            return (isFinite(constraint.minAltitude) && MathUtils.round(constraint.minAltitude, 10) > MathUtils.round(priorMaxAltitude, 10)) || requiredFpa > maxFpa;
        }
        // Start of computeFlightPathAngles helper methods
        /**
         * Finds the maximum altitude, in meters, of the constraint that defines a maximum altitude and is closest to a
         * given constraint, among all constraints prior to and including (in flight plan order) the given constraint. If a
         * vertical direct constraint is among the candidates, its minimum altitude is used if it does not define a maximum
         * altitude.
         * @param verticalPlan The vertical flight plan.
         * @param constraintIndex The index of the constraint for which to find the closest prior maximum altitude.
         * @param firstDescentConstraintIndex The index of the first descent constraint.
         * @returns The maximum altitude, in meters, of the constraint that defines a maximum altitude and is closest to the
         * specified constraint, among all constraints prior to and including (in flight plan order) the specified
         * constraint, or `Infinity` if there is no such altitude.
         */
        static findPriorMaxAltitude(verticalPlan, constraintIndex, firstDescentConstraintIndex) {
            for (let i = constraintIndex; i <= firstDescentConstraintIndex; i++) {
                const constraint = verticalPlan.constraints[i];
                if (constraint.maxAltitude < Infinity) {
                    return constraint.maxAltitude;
                }
                if (i === firstDescentConstraintIndex && constraint.type === 'direct') {
                    if (constraint.minAltitude > -Infinity) {
                        return constraint.minAltitude;
                    }
                }
            }
            return Infinity;
        }
        /**
         * Applies flight path angle and target altitude values to a sequence of constraints connected to a target constraint
         * by a constant-FPA path extending backwards from the target constraint. The target constraint defines the FPA of
         * the path.
         *
         * If the target altitude of one of the constraints in the sequence, as prescribed by the path, violates a maximum
         * altitude, the path will be terminated at the constraint immediately following (in flight plan order) the violating
         * constraint, and FPA and target altitudes will not be written to the terminating constraint or any prior
         * constraints.
         * @param verticalPlan The vertical flight plan.
         * @param targetConstraintIndex The index of the target constraint.
         * @param endConstraintIndex The index of the constraint at which the constant-FPA path ends, exclusive.
         * @param maxAltitude The maximum allowable target altitude, in meters.
         * @param out The tuple to which to write the result of the operation.
         * @returns `[index, distance]`, where `index` is the index of the constraint at which the path was terminated due to
         * violation of the maximum target altitude, or `undefined` if no constraint violated the maximum altitude, and
         * `distance` is the total distance of the path, in meters.
         */
        static applyPathValuesToSmoothedConstraints(verticalPlan, targetConstraintIndex, endConstraintIndex, maxAltitude, out) {
            const currentTargetConstraint = verticalPlan.constraints[targetConstraintIndex];
            let distance = currentTargetConstraint.distance;
            for (let i = targetConstraintIndex + 1; i < endConstraintIndex; i++) {
                const smoothedConstraint = verticalPlan.constraints[i];
                const targetAltitude = currentTargetConstraint.targetAltitude + VNavUtils.altitudeForDistance(currentTargetConstraint.fpa, distance);
                // The path can continue past the current constraint if the target altitude at the current constraint is less
                // than the maximum altitude.
                if (targetAltitude < maxAltitude) {
                    smoothedConstraint.fpa = currentTargetConstraint.fpa;
                    smoothedConstraint.targetAltitude = targetAltitude;
                    distance += smoothedConstraint.distance;
                }
                else {
                    out[0] = i;
                    out[1] = distance;
                    return out;
                }
            }
            out[0] = undefined;
            out[1] = distance;
            return out;
        }
    }
    SmoothingPathCalculator.DEFAULT_DEFAULT_FPA = 3;
    SmoothingPathCalculator.DEFAULT_MIN_FPA = 1.5;
    SmoothingPathCalculator.DEFAULT_MAX_FPA = 6;
    SmoothingPathCalculator.DEFAULT_DIRECT_TO_LEG_OFFSET = 3;

    /**
     * An implementation of {@link LNavComputerDataProvider} that sources data directly from SimVars.
     */
    class DefaultLNavComputerDataProvider {
        /**
         * Creates a new instance of DefaultLNavComputerDataProvider.
         * @param options Options with which to configure the data provider.
         */
        constructor(options) {
            var _a, _b, _c, _d;
            this.latSimVarId = SimVar.GetRegisteredId('PLANE LATITUDE', exports.SimVarValueType.Degree, '');
            this.lonSimVarId = SimVar.GetRegisteredId('PLANE LONGITUDE', exports.SimVarValueType.Degree, '');
            this.velocityXSimVarId = SimVar.GetRegisteredId('VELOCITY WORLD X', exports.SimVarValueType.Knots, '');
            this.velocityZSimVarId = SimVar.GetRegisteredId('VELOCITY WORLD Z', exports.SimVarValueType.Knots, '');
            this.headingSimVarId = SimVar.GetRegisteredId('PLANE HEADING DEGREES TRUE', exports.SimVarValueType.Degree, '');
            this.magVarSimVarId = SimVar.GetRegisteredId('MAGVAR', exports.SimVarValueType.Degree, '');
            this.tasSimVarId = SimVar.GetRegisteredId('AIRSPEED TRUE', exports.SimVarValueType.Knots, '');
            this._planePos = Value.create(new GeoPoint(NaN, NaN));
            /** @inheritDoc */
            this.planePos = this._planePos;
            this._gs = Value.create(null);
            /** @inheritDoc */
            this.gs = this._gs;
            this._track = Value.create(null);
            /** @inheritDoc */
            this.track = this._track;
            this._heading = Value.create(null);
            /** @inheritDoc */
            this.heading = this._heading;
            this._magVar = Value.create(null);
            /** @inheritDoc */
            this.magVar = this._magVar;
            this._tas = Value.create(null);
            /** @inheritDoc */
            this.tas = this._tas;
            this.isPositionDataValid = AccessibleUtils.toAccessible((_a = options === null || options === void 0 ? void 0 : options.isPositionDataValid) !== null && _a !== void 0 ? _a : true, true);
            this.isHeadingDataValid = AccessibleUtils.toAccessible((_b = options === null || options === void 0 ? void 0 : options.isHeadingDataValid) !== null && _b !== void 0 ? _b : true, true);
            this.isMagVarDataValid = AccessibleUtils.toAccessible((_c = options === null || options === void 0 ? void 0 : options.isMagVarDataValid) !== null && _c !== void 0 ? _c : true, true);
            this.isTrueAirspeedDataValid = AccessibleUtils.toAccessible((_d = options === null || options === void 0 ? void 0 : options.isTrueAirspeedDataValid) !== null && _d !== void 0 ? _d : true, true);
        }
        /**
         * Updates this provider's data.
         */
        update() {
            let heading;
            if (this.isPositionDataValid.get()) {
                this._planePos.get().set(SimVar.GetSimVarValueFastReg(this.latSimVarId), SimVar.GetSimVarValueFastReg(this.lonSimVarId));
                const velocityEW = SimVar.GetSimVarValueFastReg(this.velocityXSimVarId);
                const velocityNS = SimVar.GetSimVarValueFastReg(this.velocityZSimVarId);
                const gs = Math.hypot(velocityEW, velocityNS);
                this._gs.set(gs);
                if (gs > 1) {
                    this._track.set(MathUtils.normalizeAngleDeg(Math.atan2(velocityEW, velocityNS) * Avionics.Utils.RAD2DEG));
                }
                else {
                    this._track.set(heading = SimVar.GetSimVarValueFastReg(this.headingSimVarId));
                }
            }
            else {
                this._planePos.get().set(NaN, NaN);
                this._gs.set(null);
                this._track.set(null);
            }
            if (this.isHeadingDataValid.get()) {
                this._heading.set(heading !== null && heading !== void 0 ? heading : SimVar.GetSimVarValueFastReg(this.headingSimVarId));
            }
            else {
                this._heading.set(null);
            }
            if (this.isMagVarDataValid.get()) {
                this._magVar.set(SimVar.GetSimVarValueFastReg(this.magVarSimVarId));
            }
            else {
                this._magVar.set(null);
            }
            if (this.isTrueAirspeedDataValid.get()) {
                this._tas.set(SimVar.GetSimVarValueFastReg(this.tasSimVarId));
            }
            else {
                this._tas.set(null);
            }
        }
    }

    /**
     * LNAV transition modes.
     */
    exports.LNavTransitionMode = void 0;
    (function (LNavTransitionMode) {
        /** LNAV is attempting to track a non-transition vector. */
        LNavTransitionMode[LNavTransitionMode["None"] = 0] = "None";
        /** LNAV is attempting to track an ingress vector. */
        LNavTransitionMode[LNavTransitionMode["Ingress"] = 1] = "Ingress";
        /** LNAV is attempting to track an egress vector. */
        LNavTransitionMode[LNavTransitionMode["Egress"] = 2] = "Egress";
        /**
         * LNAV is attempting to track a non-transition vector prior to where the ingress transition joins the base flight
         * path after deactivating suspend mode.
         */
        LNavTransitionMode[LNavTransitionMode["Unsuspend"] = 3] = "Unsuspend";
    })(exports.LNavTransitionMode || (exports.LNavTransitionMode = {}));

    /**
     * A utility class for working with LNAV.
     */
    class LNavUtils {
        /**
         * Gets the flight path vectors to navigate for a leg and a given transition mode.
         * @param calc The calculations for a flight plan leg.
         * @param mode A transition mode.
         * @param isSuspended Whether sequencing is suspended.
         * @returns The flight path vectors to navigate for the given leg and transition mode.
         */
        static getVectorsForTransitionMode(calc, mode, isSuspended) {
            switch (mode) {
                case exports.LNavTransitionMode.None:
                    return isSuspended ? calc.flightPath : calc.ingressToEgress;
                case exports.LNavTransitionMode.Ingress:
                    return calc.ingress;
                case exports.LNavTransitionMode.Egress:
                    return calc.egress;
                case exports.LNavTransitionMode.Unsuspend:
                    return calc.flightPath;
            }
        }
        /**
         * Checks whether two LNAV tracking states are equal.
         * @param a The first state.
         * @param b The second state.
         * @returns Whether the two LNAV tracking states are equal.
         */
        static lnavTrackingStateEquals(a, b) {
            return a.isTracking === b.isTracking
                && a.globalLegIndex === b.globalLegIndex
                && a.transitionMode === b.transitionMode
                && a.vectorIndex === b.vectorIndex
                && a.isSuspended === b.isSuspended;
        }
        /**
         * Checks whether an index is a valid LNAV computer index.
         * @param index The index to check.
         * @returns Whether the specified index is a valid LNAV computer index.
         */
        static isValidLNavIndex(index) {
            return Number.isInteger(index) && index >= 0;
        }
        /**
         * Gets the suffix for event bus topics published by a LNAV computer with a given index.
         * @param index The index of the LNAV computer for which to get the suffix.
         * @returns The suffix for event bus topics published by a LNAV computer with the specified index.
         */
        static getEventBusTopicSuffix(index) {
            return (index === 0 ? '' : `_${index}`);
        }
    }

    /**
     * An implementation of {@link LNavEventBusTopicPublisher}.
     */
    class LNavTopicPublisher {
        /**
         * Creates a new instance of LNavTopicPublisher.
         * @param publisher The publisher to use to publish this entry's topic.
         * @param topic The topic name.
         * @param initialValue The topic's initial value.
         */
        constructor(publisher, topic, initialValue) {
            this.publisher = publisher;
            this.topic = topic;
            this.value = initialValue;
        }
        /** @inheritDoc */
        publish(value) {
            if (value !== this.value) {
                if (value !== undefined) {
                    this.value = value;
                }
                this.publisher.pub(this.topic, this.value, true, true);
            }
        }
    }
    /**
     * A computer that calculates lateral navigation guidance for an active flight plan.
     */
    class LNavComputer {
        /**
         * Creates a new instance of LNavComputer.
         * @param index The index of this computer.
         * @param bus The event bus to use with this instance.
         * @param flightPlanner The flight planner from which to source the active flight plan.
         * @param overrideModule A module that can optionally override this computer's tracking behavior.
         * @param options Options with which to configure the new computer.
         * @throws Error if `index` is not a non-negative integer.
         */
        constructor(index, bus, flightPlanner, overrideModule, options) {
            var _a, _b, _c, _d, _e;
            this.index = index;
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.overrideModule = overrideModule;
            this.vec3Cache = [Vec3Math.create(), Vec3Math.create()];
            this.geoPointCache = [new GeoPoint(0, 0), new GeoPoint(0, 0)];
            this.geoCircleCache = [new GeoCircle(Vec3Math.create(), 0)];
            this.publisher = this.bus.getPublisher();
            this.data = {
                planePos: new GeoPoint(NaN, NaN),
                gs: null,
                track: null,
                heading: null,
                magVar: null,
                tas: 0
            };
            this.currentLeg = undefined;
            this.currentVector = undefined;
            this.dtk = 0;
            this.xtk = 0;
            this.courseToSteer = null;
            this.isSteerHeading = false;
            this.alongVectorDistance = 0;
            this.vectorDistanceRemaining = 0;
            this.vectorAnticipationDistance = 0;
            this.alongTrackSpeed = 0;
            this.anticipationVector = undefined;
            this.anticipationDtk = 0;
            this.anticipationXtk = 0;
            this.inhibitNextSequence = false;
            this.trackedHeadingVector = undefined;
            this.currentState = {
                globalLegIndex: 0,
                transitionMode: exports.LNavTransitionMode.None,
                vectorIndex: 0,
                isSuspended: false,
                inhibitedSuspendLegIndex: -1,
                resetVectorsOnSuspendEnd: false,
                isMissedApproachActive: false
            };
            this.anticipationState = {
                globalLegIndex: 0,
                transitionMode: exports.LNavTransitionMode.None,
                vectorIndex: 0,
                isSuspended: false,
                inhibitedSuspendLegIndex: -1,
                resetVectorsOnSuspendEnd: false,
                isMissedApproachActive: false
            };
            this.awaitCalculateId = 0;
            this.isAwaitingCalculate = false;
            this.steerCommandBuffer = [
                { isValid: false, isHeading: false, courseToSteer: 0, trackRadius: 0, dtk: 0, xtk: 0, tae: 0 },
                { isValid: false, isHeading: false, courseToSteer: 0, trackRadius: 0, dtk: 0, xtk: 0, tae: 0 }
            ];
            this._steerCommand = Subject.create(this.steerCommandBuffer[0], (a, b) => {
                if (!a.isValid && !b.isValid) {
                    return true;
                }
                return a.isValid === b.isValid
                    && a.isHeading === b.isHeading
                    && a.courseToSteer === b.courseToSteer
                    && a.trackRadius === b.trackRadius
                    && a.dtk === b.dtk
                    && a.xtk === b.xtk
                    && a.tae === b.tae;
            });
            /** The current steering command calculated by this computer. */
            this.steerCommand = this._steerCommand;
            if (!LNavUtils.isValidLNavIndex(index)) {
                throw new Error(`LNavComputer: invalid index (${index}) specified (must be a non-negative integer)`);
            }
            this.dataProvider = (_a = options === null || options === void 0 ? void 0 : options.dataProvider) !== null && _a !== void 0 ? _a : (this.defaultDataProvider = new DefaultLNavComputerDataProvider());
            const eventBusTopicSuffix = LNavUtils.getEventBusTopicSuffix(this.index);
            this.eventBusTopicPublishers = {
                'lnav_dtk': new LNavTopicPublisher(this.publisher, `lnav_dtk${eventBusTopicSuffix}`, 0),
                'lnav_xtk': new LNavTopicPublisher(this.publisher, `lnav_xtk${eventBusTopicSuffix}`, 0),
                'lnav_is_tracking': new LNavTopicPublisher(this.publisher, `lnav_is_tracking${eventBusTopicSuffix}`, false),
                'lnav_tracked_leg_index': new LNavTopicPublisher(this.publisher, `lnav_tracked_leg_index${eventBusTopicSuffix}`, 0),
                'lnav_transition_mode': new LNavTopicPublisher(this.publisher, `lnav_transition_mode${eventBusTopicSuffix}`, exports.LNavTransitionMode.None),
                'lnav_tracked_vector_index': new LNavTopicPublisher(this.publisher, `lnav_tracked_vector_index${eventBusTopicSuffix}`, 0),
                'lnav_course_to_steer': new LNavTopicPublisher(this.publisher, `lnav_course_to_steer${eventBusTopicSuffix}`, null),
                'lnav_is_steer_heading': new LNavTopicPublisher(this.publisher, `lnav_is_steer_heading${eventBusTopicSuffix}`, false),
                'lnav_is_suspended': new LNavTopicPublisher(this.publisher, `lnav_is_suspended${eventBusTopicSuffix}`, false),
                'lnav_leg_distance_along': new LNavTopicPublisher(this.publisher, `lnav_leg_distance_along${eventBusTopicSuffix}`, 0),
                'lnav_leg_distance_remaining': new LNavTopicPublisher(this.publisher, `lnav_leg_distance_remaining${eventBusTopicSuffix}`, 0),
                'lnav_vector_distance_along': new LNavTopicPublisher(this.publisher, `lnav_vector_distance_along${eventBusTopicSuffix}`, 0),
                'lnav_vector_distance_remaining': new LNavTopicPublisher(this.publisher, `lnav_vector_distance_remaining${eventBusTopicSuffix}`, 0),
                'lnav_vector_anticipation_distance': new LNavTopicPublisher(this.publisher, `lnav_vector_anticipation_distance${eventBusTopicSuffix}`, 0),
                'lnav_along_track_speed': new LNavTopicPublisher(this.publisher, `lnav_along_track_speed${eventBusTopicSuffix}`, 0),
                'lnav_tracking_state': new LNavTopicPublisher(this.publisher, `lnav_tracking_state${eventBusTopicSuffix}`, {
                    isTracking: false,
                    globalLegIndex: 0,
                    transitionMode: exports.LNavTransitionMode.None,
                    vectorIndex: 0,
                    isSuspended: false
                }),
                'lnav_is_awaiting_calc': new LNavTopicPublisher(this.publisher, `lnav_is_awaiting_calc${eventBusTopicSuffix}`, false),
            };
            const sub = bus.getSubscriber();
            this.hasVectorAnticipation = (_b = options === null || options === void 0 ? void 0 : options.hasVectorAnticipation) !== null && _b !== void 0 ? _b : false;
            this.disableAutoSuspendAtMissedApproachPoint = (_c = options === null || options === void 0 ? void 0 : options.disableAutoSuspendAtMissedApproachPoint) !== null && _c !== void 0 ? _c : false;
            this.vectorAnticipationBankRate = (_d = options === null || options === void 0 ? void 0 : options.vectorAnticipationBankRate) !== null && _d !== void 0 ? _d : 5;
            const maxBankAngleOpt = (_e = options === null || options === void 0 ? void 0 : options.maxBankAngle) !== null && _e !== void 0 ? _e : undefined;
            switch (typeof maxBankAngleOpt) {
                case 'number':
                    this.maxBankAngleFunc = () => maxBankAngleOpt;
                    break;
                case 'function':
                    this.maxBankAngleFunc = maxBankAngleOpt;
                    break;
                default: {
                    const maxBankAngle = ConsumerSubject.create(sub.on('ap_max_bank_value'), 0);
                    this.maxBankAngleFunc = maxBankAngle.get.bind(maxBankAngle);
                }
            }
            this.interceptFunc = options === null || options === void 0 ? void 0 : options.intercept;
            sub.on(`suspend_sequencing${eventBusTopicSuffix}`).handle(suspend => {
                const flightPlan = this.flightPlanner.hasActiveFlightPlan() ? this.flightPlanner.getActiveFlightPlan() : undefined;
                if (flightPlan) {
                    // We are receiving an explicit command to suspend, so clear any suspend inhibits.
                    if (suspend) {
                        this.currentState.inhibitedSuspendLegIndex = -1;
                    }
                    this.trySetSuspended(flightPlan, this.currentState, suspend, this.currentState, false, false);
                }
            });
            sub.on(`activate_missed_approach${eventBusTopicSuffix}`).handle((v) => {
                this.currentState.isMissedApproachActive = v;
            });
            sub.on(`lnav_inhibit_next_sequence${eventBusTopicSuffix}`).handle(inhibit => {
                this.inhibitNextSequence = inhibit;
                if (inhibit) {
                    this.currentState.inhibitedSuspendLegIndex = -1;
                }
            });
            this.flightPlanner.onEvent('fplActiveLegChange').handle(e => {
                if (e.planIndex === this.flightPlanner.activePlanIndex && e.type === exports.ActiveLegType.Lateral) {
                    this.currentState.inhibitedSuspendLegIndex = -1;
                    this.resetVectors();
                }
            });
            this.flightPlanner.onEvent('fplIndexChanged').handle(() => {
                this.resetVectors();
            });
            this.flightPlanner.onEvent('fplCopied').handle((e) => {
                if (e.targetPlanIndex === this.flightPlanner.activePlanIndex) {
                    this.resetVectors();
                }
            });
            this.republishEventBusTopics();
        }
        /**
         * Resets the current vectors and transition mode.
         */
        resetVectors() {
            this.currentState.vectorIndex = 0;
            this.currentState.transitionMode = exports.LNavTransitionMode.Ingress;
            this.inhibitNextSequence = false;
            this.trackedHeadingVector = undefined;
            this.awaitCalculate();
        }
        /**
         * Checks whether a flight path vector can be tracked.
         * @param vector The flight path vector to check.
         * @returns Whether the specified flight path vector can be tracked.
         */
        canTrackVector(vector) {
            return !BitFlags.isAny(vector.flags, exports.FlightPathVectorFlags.Discontinuity)
                && vector.radius > LNavComputer.ANGULAR_TOLERANCE
                && vector.distance > LNavComputer.ANGULAR_TOLERANCE_METERS;
        }
        /**
         * Updates this computer.
         */
        update() {
            this.updateData();
            this.eventBusTopicPublishers['lnav_is_awaiting_calc'].publish(this.isAwaitingCalculate);
            let clearInhibitNextSequence = false;
            const flightPlan = this.flightPlanner.hasActiveFlightPlan() ? this.flightPlanner.getActiveFlightPlan() : undefined;
            this.currentState.globalLegIndex = flightPlan ? flightPlan.activeLateralLeg : 0;
            let isTracking = !!flightPlan
                && this.currentState.globalLegIndex <= flightPlan.length - 1
                && this.isTrackingDataValid();
            let didUpdateSteerCommand = false;
            if (flightPlan && isTracking) {
                if (this.isAwaitingCalculate) {
                    return;
                }
                const dataTrackingValid = this.data;
                this.currentLeg = flightPlan.getLeg(this.currentState.globalLegIndex);
                // We don't want to clear the inhibit next sequence flag until the active leg has been calculated
                // since we never sequence through non-calculated legs.
                clearInhibitNextSequence = !!this.currentLeg.calculated;
                this.calculateTracking(dataTrackingValid, flightPlan);
                if (this.isAwaitingCalculate) {
                    return;
                }
                if (this.hasVectorAnticipation) {
                    this.updateVectorAnticipation(dataTrackingValid, flightPlan);
                }
                isTracking = this.currentState.globalLegIndex < flightPlan.length
                    && this.currentVector !== undefined
                    && this.canTrackVector(this.currentVector);
                if (isTracking) {
                    const calcs = this.currentLeg.calculated;
                    if (this.overrideModule) {
                        if (this.overrideModule.canActivate(this.currentState, this.data)) {
                            if (!this.overrideModule.isActive()) {
                                this.overrideModule.activate(this.currentState, this.data, this.eventBusTopicPublishers);
                            }
                            this.overrideModule.update(this.currentState, this.data, this.eventBusTopicPublishers);
                            // Delegate steering command to the override module.
                            const steerCommand = this.overrideModule.getSteerCommand();
                            this.setSteerCommand(steerCommand.isValid, steerCommand.isHeading, steerCommand.courseToSteer, steerCommand.trackRadius, steerCommand.dtk, steerCommand.xtk, steerCommand.tae);
                            return;
                        }
                    }
                    isTracking = calcs !== undefined;
                    this.updateSteerCommand(dataTrackingValid);
                    didUpdateSteerCommand = true;
                }
                if (this.overrideModule) {
                    // If we have reached this point, then it means that the override module should be deactivated.
                    if (this.overrideModule.isActive()) {
                        this.overrideModule.deactivate(this.currentState);
                    }
                    this.overrideModule.update(this.currentState, this.data);
                }
            }
            else {
                if (this.overrideModule) {
                    if (this.overrideModule.isActive()) {
                        this.overrideModule.deactivate(this.currentState);
                    }
                    this.overrideModule.update(this.currentState, this.data);
                }
                // We can't be suspended if we are not tracking a flight plan.
                this.currentState.isSuspended = false;
                clearInhibitNextSequence = true;
            }
            if (!didUpdateSteerCommand) {
                this.courseToSteer = null;
                this.isSteerHeading = false;
                this.setSteerCommand(false, false, 0, 0, 0, 0, 0);
            }
            this.eventBusTopicPublishers['lnav_is_tracking'].publish(isTracking);
            this.eventBusTopicPublishers['lnav_is_suspended'].publish(this.currentState.isSuspended);
            if (isTracking) {
                const trackingStatePublisher = this.eventBusTopicPublishers['lnav_tracking_state'];
                const trackingState = trackingStatePublisher.value;
                if (trackingState.isTracking !== isTracking
                    || trackingState.globalLegIndex !== this.currentState.globalLegIndex
                    || trackingState.transitionMode !== this.currentState.transitionMode
                    || trackingState.vectorIndex !== this.currentState.vectorIndex
                    || trackingState.isSuspended !== this.currentState.isSuspended) {
                    trackingStatePublisher.publish({
                        isTracking: isTracking,
                        globalLegIndex: this.currentState.globalLegIndex,
                        transitionMode: this.currentState.transitionMode,
                        vectorIndex: this.currentState.vectorIndex,
                        isSuspended: this.currentState.isSuspended
                    });
                }
                this.eventBusTopicPublishers['lnav_dtk'].publish(this.dtk);
                this.eventBusTopicPublishers['lnav_xtk'].publish(this.xtk);
                this.eventBusTopicPublishers['lnav_tracked_leg_index'].publish(this.currentState.globalLegIndex);
                this.eventBusTopicPublishers['lnav_tracked_vector_index'].publish(this.currentState.vectorIndex);
                this.eventBusTopicPublishers['lnav_transition_mode'].publish(this.currentState.transitionMode);
                this.eventBusTopicPublishers['lnav_course_to_steer'].publish(this.courseToSteer);
                this.eventBusTopicPublishers['lnav_is_steer_heading'].publish(this.isSteerHeading);
                this.eventBusTopicPublishers['lnav_vector_distance_along'].publish(this.alongVectorDistance);
                this.eventBusTopicPublishers['lnav_vector_distance_remaining'].publish(this.vectorDistanceRemaining);
                this.eventBusTopicPublishers['lnav_vector_anticipation_distance'].publish(this.vectorAnticipationDistance);
                this.eventBusTopicPublishers['lnav_along_track_speed'].publish(this.alongTrackSpeed);
                this.eventBusTopicPublishers['lnav_leg_distance_along'].publish(this.getAlongLegDistance(flightPlan, this.currentState, this.alongVectorDistance));
                this.eventBusTopicPublishers['lnav_leg_distance_remaining'].publish(this.getLegDistanceRemaining(flightPlan, this.currentState, this.vectorDistanceRemaining));
            }
            else {
                this.currentLeg = undefined;
                this.currentVector = undefined;
                const trackingStatePublisher = this.eventBusTopicPublishers['lnav_tracking_state'];
                const trackingState = trackingStatePublisher.value;
                if (trackingState.isTracking
                    || trackingState.globalLegIndex !== 0
                    || trackingState.transitionMode !== exports.LNavTransitionMode.None
                    || trackingState.vectorIndex !== 0
                    || trackingState.isSuspended !== this.currentState.isSuspended) {
                    trackingStatePublisher.publish({
                        isTracking: false,
                        globalLegIndex: 0,
                        transitionMode: exports.LNavTransitionMode.None,
                        vectorIndex: 0,
                        isSuspended: this.currentState.isSuspended
                    });
                }
                this.eventBusTopicPublishers['lnav_dtk'].publish(0);
                this.eventBusTopicPublishers['lnav_xtk'].publish(0);
                this.eventBusTopicPublishers['lnav_tracked_leg_index'].publish(0);
                this.eventBusTopicPublishers['lnav_tracked_vector_index'].publish(0);
                this.eventBusTopicPublishers['lnav_transition_mode'].publish(exports.LNavTransitionMode.None);
                this.eventBusTopicPublishers['lnav_course_to_steer'].publish(null);
                this.eventBusTopicPublishers['lnav_is_steer_heading'].publish(false);
                this.eventBusTopicPublishers['lnav_vector_distance_along'].publish(0);
                this.eventBusTopicPublishers['lnav_vector_distance_remaining'].publish(0);
                this.eventBusTopicPublishers['lnav_vector_anticipation_distance'].publish(0);
                this.eventBusTopicPublishers['lnav_along_track_speed'].publish(0);
                this.eventBusTopicPublishers['lnav_leg_distance_along'].publish(0);
                this.eventBusTopicPublishers['lnav_leg_distance_remaining'].publish(0);
            }
            this.inhibitNextSequence && (this.inhibitNextSequence = !clearInhibitNextSequence);
        }
        /**
         * Immediately republishes all event bus topics with their current values.
         */
        republishEventBusTopics() {
            for (const topic in this.eventBusTopicPublishers) {
                this.eventBusTopicPublishers[topic].publish();
            }
        }
        /**
         * Updates this computer's data.
         */
        updateData() {
            var _a;
            (_a = this.defaultDataProvider) === null || _a === void 0 ? void 0 : _a.update();
            this.data.planePos.set(this.dataProvider.planePos.get());
            this.data.gs = this.dataProvider.gs.get();
            this.data.track = this.dataProvider.track.get();
            this.data.heading = this.dataProvider.heading.get();
            this.data.magVar = this.dataProvider.magVar.get();
            this.data.tas = this.dataProvider.tas.get();
        }
        /**
         * Checks whether the current LNAV data contains valid tracking data.
         * @returns Whether the current LNAV data contains valid tracking data.
         */
        isTrackingDataValid() {
            return this.data.planePos.isValid()
                && this.data.gs !== null
                && this.data.track !== null;
        }
        /**
         * Updates this computer's steering command using guidance generated from this computer's currently tracked flight
         * path vector.
         * @param data The current LNAV data with valid tracking data.
         */
        updateSteerCommand(data) {
            let vector = undefined;
            let trackRadius = 0;
            let dtk = 0;
            let xtk = 0;
            let tae = 0;
            if (this.anticipationVector
                && this.vectorAnticipationDistance > 0
                && this.vectorDistanceRemaining <= this.vectorAnticipationDistance
                // Do not fly the anticipated vector if our current-vector crosstrack error is greater than the anticipated
                // vector's radius. This keeps us from flying the wrong "side" of an anticipated vector.
                && Math.abs(this.xtk) < UnitType.GA_RADIAN.convertTo(FlightPathUtils.getVectorTurnRadius(this.anticipationVector), UnitType.NMILE)) {
                vector = this.anticipationVector;
                dtk = this.anticipationDtk;
                xtk = this.anticipationXtk;
                trackRadius = this.anticipationVector.radius;
            }
            else if (this.currentVector
                && this.canTrackVector(this.currentVector)) {
                vector = this.currentVector;
                dtk = this.dtk;
                xtk = this.xtk;
                trackRadius = this.currentVector.radius;
            }
            if (vector) {
                this.isSteerHeading = vector.heading !== null && BitFlags.isAll(vector.flags, exports.FlightPathVectorFlags.ConstantHeading);
                if (this.isSteerHeading) {
                    if (data.heading !== null) {
                        this.courseToSteer = this.getCourseToSteerHeading(data, vector);
                        trackRadius = 0;
                        dtk = 0;
                        xtk = 0;
                        tae = (MathUtils.angularDistanceDeg(this.courseToSteer, data.heading, 1) + 180) % 360 - 180;
                    }
                    else {
                        this.courseToSteer = null;
                        this.isSteerHeading = false;
                    }
                }
                else {
                    this.courseToSteer = this.getCourseToSteerTrack(data, dtk, xtk);
                    tae = (MathUtils.angularDistanceDeg(dtk, data.track, 1) + 180) % 360 - 180;
                }
            }
            else {
                this.courseToSteer = null;
                this.isSteerHeading = false;
            }
            if (this.courseToSteer === null) {
                this.setSteerCommand(false, false, 0, 0, 0, 0, 0);
            }
            else {
                this.setSteerCommand(true, this.isSteerHeading, this.courseToSteer, trackRadius, dtk, xtk, tae);
            }
        }
        /**
         * Gets a true course to steer to follow a track, in degrees.
         * @param data The current LNAV data with valid tracking data.
         * @param dtk The desired true track, in degrees.
         * @param xtk The cross-track error, in nautical miles.
         * @returns The true course to steer to follow the specified track, in degrees.
         */
        getCourseToSteerTrack(data, dtk, xtk) {
            var _a;
            let absInterceptAngle;
            if (this.interceptFunc !== undefined) {
                absInterceptAngle = this.interceptFunc(dtk, xtk, (_a = data.tas) !== null && _a !== void 0 ? _a : data.gs);
            }
            else {
                absInterceptAngle = Math.min(Math.pow(Math.abs(xtk) * 20, 1.35) + (Math.abs(xtk) * 50), 45);
                if (absInterceptAngle <= 2.5) {
                    absInterceptAngle = NavMath.clamp(Math.abs(xtk * 150), 0, 2.5);
                }
            }
            const interceptAngle = xtk < 0 ? absInterceptAngle : -1 * absInterceptAngle;
            return MathUtils.normalizeAngleDeg(dtk + interceptAngle);
        }
        /**
         * Gets a true course to steer to follow a heading, in degrees.
         * @param data The current LNAV data with valid tracking data.
         * @param vector The flight path vector that defines the heading to follow.
         * @returns The true course to steer to follow the specified heading, in degrees.
         */
        getCourseToSteerHeading(data, vector) {
            return vector.isHeadingTrue || data.magVar === null
                ? vector.heading
                : MagVar.magneticToTrue(vector.heading, data.magVar);
        }
        /**
         * Sets this computer's steering command.
         * @param isValid Whether the steering command is valid.
         * @param isHeading Whether the command is attempting to steer toward a heading instead of a track.
         * @param courseToSteer The true course to steer, in degrees.
         * @param trackRadius The radius of the track toward which the command is attempting to steer, in great-arc radians.
         * A radius of `pi / 2` indicates the track is a great circle. A radius less than `pi / 2` indicates the track turns
         * to the left. A radius greater than `pi / 2` indicates the track turns to the right.
         * @param dtk The current desired true track, in degrees.
         * @param xtk The current cross-track error, in nautical miles. Positive values indicate that the plane is to the
         * right of the desired track.
         * @param tae The current track angle error, in degrees in the range `[-180, 180)`.
         */
        setSteerCommand(isValid, isHeading, courseToSteer, trackRadius, dtk, xtk, tae) {
            const steerCommandBufferActiveIndex = this._steerCommand.get() === this.steerCommandBuffer[0] ? 0 : 1;
            const command = this.steerCommandBuffer[(steerCommandBufferActiveIndex + 1) % 2];
            command.isValid = isValid;
            command.isHeading = isHeading;
            command.courseToSteer = courseToSteer;
            command.trackRadius = trackRadius;
            command.dtk = dtk;
            command.xtk = xtk;
            command.tae = tae;
            this._steerCommand.set(command);
        }
        /**
         * Calculates the tracking from the current leg.
         * @param data The current LNAV data with valid tracking data.
         * @param plan The active flight plan.
         */
        calculateTracking(data, plan) {
            var _a, _b;
            let didAdvance;
            do {
                didAdvance = false;
                if (!this.currentLeg) {
                    break;
                }
                // Don't really need to fly the intial leg?
                if (this.currentLeg.leg.type === exports.LegType.IF && this.currentState.globalLegIndex === 0 && plan.length > 1) {
                    this.currentLeg = plan.getLeg(++this.currentState.globalLegIndex);
                    plan.setCalculatingLeg(this.currentState.globalLegIndex);
                    plan.setLateralLeg(this.currentState.globalLegIndex);
                    this.trackedHeadingVector = undefined;
                    continue;
                }
                const transitionMode = this.currentState.transitionMode;
                const legIndex = this.currentState.globalLegIndex;
                const vectorIndex = this.currentState.vectorIndex;
                const isSuspended = this.currentState.isSuspended;
                const calcs = this.currentLeg.calculated;
                if (calcs) {
                    // Heading legs can be recalculated while they are active. When that happens, we want to reset the tracked
                    // vector index to the first vector so that we don't "skip" over any recalculated vectors.
                    if (this.trackedHeadingVector
                        && (this.currentState.transitionMode === exports.LNavTransitionMode.Egress
                            || this.currentState.vectorIndex > 0)) {
                        const vectors = LNavUtils.getVectorsForTransitionMode(calcs, this.currentState.transitionMode, this.currentState.isSuspended);
                        const vector = vectors[this.currentState.vectorIndex];
                        if (!vector
                            || vector.startLat !== this.trackedHeadingVector.startLat
                            || vector.startLon !== this.trackedHeadingVector.startLon
                            || vector.centerX !== this.trackedHeadingVector.centerX
                            || vector.centerY !== this.trackedHeadingVector.centerY
                            || vector.centerZ !== this.trackedHeadingVector.centerZ
                            || vector.radius !== this.trackedHeadingVector.radius) {
                            // Heading legs don't have ingress transitions.
                            this.currentState.transitionMode = exports.LNavTransitionMode.None;
                            this.currentState.vectorIndex = 0;
                        }
                    }
                    const vectors = LNavUtils.getVectorsForTransitionMode(calcs, this.currentState.transitionMode, this.currentState.isSuspended);
                    const vector = vectors[this.currentState.vectorIndex];
                    const isVectorValid = vector && this.canTrackVector(vector);
                    const isUnsuspendInvalid = this.currentState.transitionMode === exports.LNavTransitionMode.Unsuspend
                        && (calcs.ingress.length === 0 || calcs.flightPath[calcs.ingressJoinIndex] === undefined);
                    if (isVectorValid && !isUnsuspendInvalid) {
                        const planePos = data.planePos;
                        const circle = FlightPathUtils.setGeoCircleFromVector(vector, this.geoCircleCache[0]);
                        const start = GeoPoint.sphericalToCartesian(vector.startLat, vector.startLon, this.vec3Cache[0]);
                        let endLat, endLon;
                        let end;
                        let vectorDistanceNM;
                        // If we are in unsuspend mode and tracking the vector at which the ingress transition joins the base flight
                        // path, then we treat the point at which the ingress joins the vector as the de-facto end of the vector,
                        // because at that point we want to sequence into the ingress-to-egress vector array. In all other cases,
                        // we use the entire length of the tracked vector.
                        if (transitionMode === exports.LNavTransitionMode.Unsuspend && vectorIndex === calcs.ingressJoinIndex && calcs.ingress.length > 0) {
                            const lastIngressVector = calcs.ingress[calcs.ingress.length - 1];
                            endLat = lastIngressVector.endLat;
                            endLon = lastIngressVector.endLon;
                            end = GeoPoint.sphericalToCartesian(endLat, endLon, this.vec3Cache[1]);
                            vectorDistanceNM = UnitType.GA_RADIAN.convertTo(circle.distanceAlong(start, end, Math.PI), UnitType.NMILE);
                        }
                        else {
                            endLat = vector.endLat;
                            endLon = vector.endLon;
                            end = GeoPoint.sphericalToCartesian(endLat, endLon, this.vec3Cache[1]);
                            vectorDistanceNM = UnitType.METER.convertTo(vector.distance, UnitType.NMILE);
                        }
                        this.xtk = UnitType.GA_RADIAN.convertTo(circle.distance(planePos), UnitType.NMILE);
                        this.dtk = circle.bearingAt(planePos, Math.PI);
                        const alongTrackSpeed = FlightPathUtils.projectVelocityToCircle(data.gs, planePos, data.track, circle);
                        this.alongTrackSpeed = isNaN(alongTrackSpeed) ? data.gs : alongTrackSpeed;
                        const normDist = FlightPathUtils.getAlongArcNormalizedDistance(circle, start, end, planePos);
                        this.alongVectorDistance = normDist * vectorDistanceNM;
                        this.vectorDistanceRemaining = (1 - normDist) * vectorDistanceNM;
                        if (normDist > 1) {
                            this.advanceToNextVector(plan, this.currentState, true, this.currentState);
                        }
                    }
                    else {
                        this.alongVectorDistance = 0;
                        this.vectorDistanceRemaining = 0;
                        this.vectorAnticipationDistance = 0;
                        this.advanceToNextVector(plan, this.currentState, true, this.currentState);
                    }
                    didAdvance = transitionMode !== this.currentState.transitionMode
                        || legIndex !== this.currentState.globalLegIndex
                        || vectorIndex !== this.currentState.vectorIndex
                        || isSuspended !== this.currentState.isSuspended;
                    if (legIndex !== this.currentState.globalLegIndex) {
                        this.currentLeg = (_a = plan.tryGetLeg(this.currentState.globalLegIndex)) !== null && _a !== void 0 ? _a : undefined;
                        plan.setCalculatingLeg(this.currentState.globalLegIndex);
                        plan.setLateralLeg(this.currentState.globalLegIndex);
                    }
                }
            } while (!this.isAwaitingCalculate && didAdvance && this.currentState.globalLegIndex <= plan.length - 1);
            if (this.currentState.transitionMode === exports.LNavTransitionMode.Egress
                && this.currentState.globalLegIndex + 1 < plan.length
                && plan.activeCalculatingLeg !== this.currentState.globalLegIndex + 1) {
                plan.setCalculatingLeg(this.currentState.globalLegIndex + 1);
            }
            this.currentVector = ((_b = this.currentLeg) === null || _b === void 0 ? void 0 : _b.calculated)
                ? LNavUtils.getVectorsForTransitionMode(this.currentLeg.calculated, this.currentState.transitionMode, this.currentState.isSuspended)[this.currentState.vectorIndex]
                : undefined;
            this.trackedHeadingVector = this.currentVector !== undefined && this.currentVector.heading !== null ? this.currentVector : undefined;
        }
        /**
         * Updates this director's vector anticipation data, including the anticipation distance, DTK and XTK for the
         * anticipated vector, and bearing from the airplane to the end of the anticipated vector.
         * @param data The current LNAV data with valid tracking data.
         * @param plan The active flight plan.
         */
        updateVectorAnticipation(data, plan) {
            var _a;
            this.anticipationVector = undefined;
            this.vectorAnticipationDistance = 0;
            this.anticipationDtk = 0;
            this.anticipationXtk = 0;
            if (!this.currentVector || !this.canTrackVector(this.currentVector)) {
                return;
            }
            // Find the vector that will be tracked after we sequence past the current one.
            this.advanceToNextVector(plan, this.currentState, false, this.anticipationState);
            const anticipationCalcs = (_a = plan.tryGetLeg(this.anticipationState.globalLegIndex)) === null || _a === void 0 ? void 0 : _a.calculated;
            if (!anticipationCalcs) {
                return;
            }
            const anticipationVectors = LNavUtils.getVectorsForTransitionMode(anticipationCalcs, this.anticipationState.transitionMode, this.anticipationState.isSuspended);
            this.anticipationVector = anticipationVectors[this.anticipationState.vectorIndex];
            if (!this.anticipationVector
                || this.anticipationVector === this.currentVector
                || !this.canTrackVector(this.anticipationVector)) {
                this.anticipationVector = undefined;
                return;
            }
            const circle = FlightPathUtils.setGeoCircleFromVector(this.anticipationVector, this.geoCircleCache[0]);
            this.anticipationXtk = UnitType.GA_RADIAN.convertTo(circle.distance(data.planePos), UnitType.NMILE);
            this.anticipationDtk = circle.bearingAt(data.planePos, Math.PI);
            // Find the bank angles that are required to keep the airplane following the current and anticipated vectors
            // assuming zero XTK error and wind. Then approximate how long it will take the airplane to roll from one to the
            // other -> this will be the anticipation time. Finally, convert the anticipation time to a distance by multiplying
            // by along-track speed.
            const maxBankAngle = this.maxBankAngleFunc();
            const currentVectorIdealBankAngle = MathUtils.clamp(LNavComputer.getVectorIdealBankAngle(this.currentVector, data.gs), -maxBankAngle, maxBankAngle);
            const anticipationIdealBankAngle = MathUtils.clamp(LNavComputer.getVectorIdealBankAngle(this.anticipationVector, data.gs), -maxBankAngle, maxBankAngle);
            const deltaBank = Math.abs(currentVectorIdealBankAngle - anticipationIdealBankAngle);
            const rollTimeSeconds = deltaBank / this.vectorAnticipationBankRate;
            this.vectorAnticipationDistance = Math.min(rollTimeSeconds / 3600 * this.alongTrackSpeed, 
            // Limit vector anticipation to the radius of the anticipated vector so that we don't start flying anticipated
            // arc/turn vectors too early with a large XTK error and veer off in the wrong direction.
            UnitType.GA_RADIAN.convertTo(FlightPathUtils.getVectorTurnRadius(this.anticipationVector), UnitType.NMILE));
        }
        /**
         * Applies suspends that apply at the end of a leg.
         * @param plan The active flight plan.
         * @param state The current LNAV state.
         * @param out The LNAV state to which to write.
         * @returns The LNAV state after applying end-of-leg suspends.
         */
        applyEndOfLegSuspends(plan, state, out) {
            if (state !== out) {
                LNavComputer.copyStateInfo(state, out);
            }
            const leg = plan.tryGetLeg(state.globalLegIndex);
            if (!leg) {
                return out;
            }
            // Do not allow suspend on thru discontinuities.
            const inhibitNextSequence = this.inhibitNextSequence
                && leg.leg.type !== exports.LegType.ThruDiscontinuity;
            if (leg.leg.type === exports.LegType.FM || leg.leg.type === exports.LegType.VM || leg.leg.type === exports.LegType.Discontinuity) {
                return this.trySetSuspended(plan, state, true, out, true, false);
            }
            else if (inhibitNextSequence) {
                return this.trySetSuspended(plan, state, true, out, false, true);
            }
            else if (state.globalLegIndex < plan.length - 1) {
                const nextLeg = plan.getLeg(state.globalLegIndex + 1);
                if (!this.disableAutoSuspendAtMissedApproachPoint
                    && !state.isMissedApproachActive
                    && (leg.leg.fixTypeFlags === exports.FixTypeFlags.MAP
                        || (!BitFlags.isAll(leg.flags, exports.LegDefinitionFlags.MissedApproach) && BitFlags.isAll(nextLeg.flags, exports.LegDefinitionFlags.MissedApproach)))) {
                    return this.trySetSuspended(plan, state, true, out, true, false);
                }
            }
            return out;
        }
        /**
         * Applies suspends that apply at the beginning of a leg.
         * @param plan The active flight plan.
         * @param state The current LNAV state.
         * @param out The LNAV state to which to write.
         * @returns The LNAV state after applying start-of-leg suspends.
         */
        applyStartOfLegSuspends(plan, state, out) {
            if (state !== out) {
                LNavComputer.copyStateInfo(state, out);
            }
            const leg = plan.getLeg(state.globalLegIndex);
            if (!leg) {
                return out;
            }
            if (leg.leg.type === exports.LegType.HM || state.globalLegIndex === plan.length - 1) {
                return this.trySetSuspended(plan, state, true, out, false, false);
            }
            return out;
        }
        /**
         * Advances an LNAV state to the next trackable vector.
         * @param plan The active flight plan.
         * @param state The state from which to advance.
         * @param awaitCalculateOnNextLeg Whether to await leg calculations when advancing to the next leg. If `true`, the
         * state will only advance as far as the first vector of the next leg.
         * @param out The state to which to write the results.
         * @returns The LNAV state after advancing to the next trackable vector.
         */
        advanceToNextVector(plan, state, awaitCalculateOnNextLeg, out) {
            var _a, _b, _c, _d, _e, _f;
            if (state !== out) {
                LNavComputer.copyStateInfo(state, out);
            }
            let leg = plan.tryGetLeg(state.globalLegIndex);
            if (!leg) {
                return out;
            }
            let legIndex = state.globalLegIndex;
            let transitionMode = state.transitionMode;
            let isSuspended = state.isSuspended;
            let vectors = leg.calculated ? LNavUtils.getVectorsForTransitionMode(leg.calculated, transitionMode, isSuspended) : undefined;
            let vectorIndex = state.vectorIndex + 1;
            let vectorEndIndex = (_a = vectors === null || vectors === void 0 ? void 0 : vectors.length) !== null && _a !== void 0 ? _a : 0;
            let didAdvance = false;
            let isDone = false;
            // If we are in unsuspended mode, we are tracking the base flight path vector array, and we want to switch to the
            // ingress-to-egress array when we reach the vector at which the ingress transition joins the base flight path.
            if (transitionMode === exports.LNavTransitionMode.Unsuspend && leg.calculated) {
                if (leg.calculated.ingressJoinIndex < 0) {
                    vectorEndIndex = 0;
                }
                else {
                    const ingress = leg.calculated.ingress;
                    const ingressJoinVector = leg.calculated.flightPath[leg.calculated.ingressJoinIndex];
                    // If the ingress joins the base flight path at the beginning of the joined vector, then we want to switch to
                    // the ingress-to-egress array once we reach the joined vector. Otherwise, we want to switch when we pass the
                    // joined vector.
                    if (ingress.length > 0
                        && ingressJoinVector
                        && GeoPoint.equals(ingress[ingress.length - 1].endLat, ingress[ingress.length - 1].endLon, ingressJoinVector.startLat, ingressJoinVector.startLon)) {
                        vectorEndIndex = leg.calculated.ingressJoinIndex;
                    }
                    else {
                        vectorEndIndex = leg.calculated.ingressJoinIndex + 1;
                    }
                }
            }
            // Continue advancing until we reach a trackable vector.
            while (!vectors || vectorIndex >= vectorEndIndex || !this.canTrackVector(vectors[vectorIndex])) {
                switch (transitionMode) {
                    case exports.LNavTransitionMode.Ingress:
                        transitionMode = exports.LNavTransitionMode.None;
                        vectors = leg.calculated ? LNavUtils.getVectorsForTransitionMode(leg.calculated, transitionMode, isSuspended) : undefined;
                        vectorIndex = Math.max(0, isSuspended ? (_c = (_b = leg.calculated) === null || _b === void 0 ? void 0 : _b.ingressJoinIndex) !== null && _c !== void 0 ? _c : 0 : 0);
                        didAdvance = true;
                        break;
                    case exports.LNavTransitionMode.Unsuspend:
                        transitionMode = exports.LNavTransitionMode.None;
                        vectors = (_d = leg.calculated) === null || _d === void 0 ? void 0 : _d.ingressToEgress;
                        vectorIndex = 0;
                        didAdvance = true;
                        break;
                    case exports.LNavTransitionMode.None:
                        if (!isSuspended) {
                            transitionMode = exports.LNavTransitionMode.Egress;
                            vectors = leg.calculated ? LNavUtils.getVectorsForTransitionMode(leg.calculated, transitionMode, isSuspended) : undefined;
                            vectorIndex = 0;
                            didAdvance = true;
                        }
                        else if (leg.leg.type === exports.LegType.HM) {
                            vectors = (_e = leg.calculated) === null || _e === void 0 ? void 0 : _e.flightPath;
                            vectorIndex = 0;
                            didAdvance = true;
                        }
                        else {
                            if (!didAdvance && vectors) {
                                vectorIndex = Math.max(0, vectors.length - 1);
                            }
                            isDone = true;
                        }
                        break;
                    case exports.LNavTransitionMode.Egress:
                        out.globalLegIndex = legIndex;
                        out.transitionMode = transitionMode;
                        out.vectorIndex = vectorIndex;
                        out.isSuspended = isSuspended;
                        this.advanceToNextLeg(plan, out, out);
                        // If we are awaiting calculate when advancing to the next leg or if we can't advance to the next leg,
                        // we are done since either way we cannot advance any farther.
                        if (awaitCalculateOnNextLeg || out.globalLegIndex === legIndex) {
                            return out;
                        }
                        leg = plan.tryGetLeg(out.globalLegIndex);
                        if (!(leg === null || leg === void 0 ? void 0 : leg.calculated)) {
                            // If the next leg is not calculated yet, we can't advance any farther because we don't know what the
                            // vectors will be when the leg is calculated.
                            return out;
                        }
                        else {
                            legIndex = out.globalLegIndex;
                            transitionMode = out.transitionMode;
                            vectors = LNavUtils.getVectorsForTransitionMode(leg.calculated, out.transitionMode, out.isSuspended);
                            vectorIndex = out.vectorIndex;
                            isSuspended = out.isSuspended;
                            didAdvance = false;
                        }
                }
                if (isDone) {
                    break;
                }
                vectorEndIndex = (_f = vectors === null || vectors === void 0 ? void 0 : vectors.length) !== null && _f !== void 0 ? _f : 0;
            }
            out.globalLegIndex = legIndex;
            out.transitionMode = transitionMode;
            out.vectorIndex = vectorIndex;
            out.isSuspended = isSuspended;
            return out;
        }
        /**
         * Advances an LNAV state to the next leg.
         * @param plan The active flight plan.
         * @param state The state from which to advance.
         * @param out The state to which to write the results.
         * @returns The LNAV state after advancing to the next leg.
         */
        advanceToNextLeg(plan, state, out) {
            var _a, _b, _c;
            this.applyEndOfLegSuspends(plan, state, out);
            if (!out.isSuspended) {
                if (out.globalLegIndex + 1 >= plan.length) {
                    out.transitionMode = exports.LNavTransitionMode.None;
                    out.vectorIndex = Math.max(0, ((_c = (_b = (_a = plan.tryGetLeg(out.globalLegIndex)) === null || _a === void 0 ? void 0 : _a.calculated) === null || _b === void 0 ? void 0 : _b.flightPath.length) !== null && _c !== void 0 ? _c : 0) - 1);
                    return out;
                }
                out.globalLegIndex++;
                out.transitionMode = exports.LNavTransitionMode.Ingress;
                out.vectorIndex = 0;
                out.inhibitedSuspendLegIndex = -1;
                this.applyStartOfLegSuspends(plan, out, out);
            }
            return out;
        }
        /**
         * Attempts to activate/deactivate suspend on an LNAV state.
         * @param plan The active flight plan.
         * @param state The state for which to set suspended.
         * @param suspend The suspended state to set.
         * @param out The state to which to write the results.
         * @param inhibitResuspend Whether to inhibit resuspend of the suspended leg once suspend ends on that leg. Ignored
         * if `suspend` is `false`. Defaults to `false`.
         * @param resetVectorsOnSuspendEnd Whether to reset the tracked vector to the beginning of the suspended leg once
         * suspend ends on that leg. Ignored if `suspend` is `false`. Defaults to `false`.
         * @returns The LNAV state after the suspend state has been set.
         */
        trySetSuspended(plan, state, suspend, out, inhibitResuspend = false, resetVectorsOnSuspendEnd = false) {
            var _a;
            if (state !== out) {
                LNavComputer.copyStateInfo(state, out);
            }
            if (suspend && state.globalLegIndex === state.inhibitedSuspendLegIndex) {
                return out;
            }
            if (suspend) {
                out.inhibitedSuspendLegIndex = inhibitResuspend ? state.globalLegIndex : -1;
                out.resetVectorsOnSuspendEnd = resetVectorsOnSuspendEnd;
            }
            if (state.isSuspended !== suspend) {
                out.isSuspended = suspend;
                if (!suspend && state.resetVectorsOnSuspendEnd) {
                    out.transitionMode = exports.LNavTransitionMode.None;
                    out.vectorIndex = 0;
                    out.resetVectorsOnSuspendEnd = false;
                }
                else {
                    const leg = plan.tryGetLeg(state.globalLegIndex);
                    const legCalc = leg === null || leg === void 0 ? void 0 : leg.calculated;
                    const ingressJoinVector = legCalc === null || legCalc === void 0 ? void 0 : legCalc.flightPath[legCalc.ingressJoinIndex];
                    if (legCalc
                        && state.transitionMode === exports.LNavTransitionMode.None
                        && legCalc.ingressJoinIndex >= 0
                        && ingressJoinVector
                        && legCalc.ingress.length > 0) {
                        // Because we are switching between tracking the base flight path vector array and the ingress-to-egress
                        // array, we need to reconcile the vector index.
                        const lastIngressVector = legCalc.ingress[legCalc.ingress.length - 1];
                        let vectors;
                        let offset;
                        if (suspend) {
                            // Unsuspended -> Suspended.
                            vectors = legCalc.flightPath;
                            if (GeoPoint.equals(lastIngressVector.endLat, lastIngressVector.endLon, ingressJoinVector.endLat, ingressJoinVector.endLon)) {
                                offset = legCalc.ingressJoinIndex + 1;
                            }
                            else {
                                offset = legCalc.ingressJoinIndex;
                            }
                        }
                        else {
                            // Suspended -> Unsuspended.
                            let pastIngressJoin = state.vectorIndex > legCalc.ingressJoinIndex;
                            if (!pastIngressJoin
                                && state.vectorIndex === legCalc.ingressJoinIndex
                                && legCalc.flightPath[legCalc.ingressJoinIndex]
                                && this.data.planePos.isValid()) {
                                const vector = legCalc.flightPath[legCalc.ingressJoinIndex];
                                const circle = FlightPathUtils.setGeoCircleFromVector(vector, this.geoCircleCache[0]);
                                const start = GeoPoint.sphericalToCartesian(vector.startLat, vector.startLon, this.vec3Cache[0]);
                                const end = GeoPoint.sphericalToCartesian(ingressJoinVector.endLat, ingressJoinVector.endLon, this.vec3Cache[1]);
                                pastIngressJoin = FlightPathUtils.getAlongArcNormalizedDistance(circle, start, end, this.data.planePos) >= 1;
                            }
                            if (pastIngressJoin) {
                                vectors = legCalc.ingressToEgress;
                                if (GeoPoint.equals(lastIngressVector.endLat, lastIngressVector.endLon, ingressJoinVector.endLat, ingressJoinVector.endLon)) {
                                    offset = -(legCalc.ingressJoinIndex + 1);
                                }
                                else {
                                    offset = -legCalc.ingressJoinIndex;
                                }
                            }
                            else {
                                vectors = legCalc.flightPath;
                                offset = 0;
                                out.transitionMode = exports.LNavTransitionMode.Unsuspend;
                            }
                        }
                        // Not using Utils.Clamp() because I need it to clamp to >=0 last.
                        out.vectorIndex = Math.max(0, Math.min(state.vectorIndex + offset, vectors.length - 1));
                    }
                    // If we are in unsuspend mode and have become suspended again, change the transition mode back to none. Vector
                    // index stays the same because we are tracking the base flight path vector array both before and after.
                    if (suspend && state.transitionMode === exports.LNavTransitionMode.Unsuspend) {
                        out.transitionMode = exports.LNavTransitionMode.None;
                    }
                    if (suspend && state.transitionMode === exports.LNavTransitionMode.Egress) {
                        out.transitionMode = exports.LNavTransitionMode.None;
                        out.vectorIndex = Math.max(0, ((_a = legCalc === null || legCalc === void 0 ? void 0 : legCalc.flightPath.length) !== null && _a !== void 0 ? _a : 1) - 1);
                    }
                }
            }
            return out;
        }
        /**
         * Awaits a flight plan calculation. Starts a calculation of the active flight plan and suspends all tracking and
         * sequencing until the calculation is finished. If this method is called while a previous execution is still
         * awaiting, the new await takes precedence.
         */
        async awaitCalculate() {
            if (!this.flightPlanner.hasActiveFlightPlan()) {
                return;
            }
            this.isAwaitingCalculate = true;
            const id = ++this.awaitCalculateId;
            const plan = this.flightPlanner.getActiveFlightPlan();
            try {
                await plan.calculate();
            }
            catch ( /* continue */_a) { /* continue */ }
            if (id !== this.awaitCalculateId) {
                return;
            }
            this.isAwaitingCalculate = false;
        }
        /**
         * Gets an along-track distance from the start of a tracked flight plan leg given a distance along a tracked vector.
         * @param plan The active flight plan.
         * @param state The LNAV state.
         * @param alongVectorDistance The along-track distance from the start of the tracked vector, in nautical miles.
         * @returns The along-track distance, in nautical miles, from the start of the specified flight plan leg given the
         * specified state and along-vector distance.
         */
        getAlongLegDistance(plan, state, alongVectorDistance) {
            var _a;
            const calcs = (_a = plan.tryGetLeg(state.globalLegIndex)) === null || _a === void 0 ? void 0 : _a.calculated;
            if (!calcs) {
                return 0;
            }
            let vectors = LNavUtils.getVectorsForTransitionMode(calcs, state.transitionMode, false);
            const vector = vectors[state.vectorIndex];
            if (!vector) {
                return 0;
            }
            let distanceAlong = 0;
            for (let i = state.vectorIndex - 1; i >= 0; i--) {
                distanceAlong += vectors[i].distance;
            }
            switch (state.transitionMode) {
                case exports.LNavTransitionMode.Egress:
                    vectors = calcs.ingressToEgress;
                    for (let i = vectors.length - 1; i >= 0; i--) {
                        distanceAlong += vectors[i].distance;
                    }
                // eslint-disable-next-line no-fallthrough
                case exports.LNavTransitionMode.None:
                case exports.LNavTransitionMode.Unsuspend:
                    vectors = calcs.ingress;
                    for (let i = vectors.length - 1; i >= 0; i--) {
                        distanceAlong += vectors[i].distance;
                    }
            }
            if (state.transitionMode === exports.LNavTransitionMode.Unsuspend) {
                const lastIngressVector = calcs.ingress[calcs.ingress.length - 1];
                const ingressJoinVector = calcs.flightPath[calcs.ingressJoinIndex];
                if (ingressJoinVector && lastIngressVector) {
                    // If we are in unsuspend mode and a valid ingress transition exists, then we need to subtract the distance
                    // from the start of the current vector to where the ingress transition joins the base flight path.
                    for (let i = state.vectorIndex; i < calcs.ingressJoinIndex; i++) {
                        distanceAlong -= vectors[i].distance;
                    }
                    // If the current vector is before or equal to the vector at which the ingress joins the base flight path, we
                    // need to subtract the distance from the start of the joined vector to where the ingress joins.
                    if (state.vectorIndex <= calcs.ingressJoinIndex) {
                        const circle = FlightPathUtils.setGeoCircleFromVector(ingressJoinVector, this.geoCircleCache[0]);
                        const start = GeoPoint.sphericalToCartesian(ingressJoinVector.startLat, ingressJoinVector.startLon, this.vec3Cache[0]);
                        const end = GeoPoint.sphericalToCartesian(lastIngressVector.endLat, lastIngressVector.endLon, this.vec3Cache[1]);
                        distanceAlong -= UnitType.GA_RADIAN.convertTo(circle.distanceAlong(start, end, Math.PI), UnitType.METER);
                    }
                }
            }
            return UnitType.METER.convertTo(distanceAlong, UnitType.NMILE) + alongVectorDistance;
        }
        /**
         * Gets an along-track distance from the end of a tracked flight plan leg given a distance remaining along a tracked
         * vector.
         * @param plan The active flight plan.
         * @param state The LNAV state.
         * @param vectorDistanceRemaining The along-track distance from the end of the tracked vector, in nautical miles.
         * @returns The along-track distance, in nautical miles, from the end of the specified flight plan leg given the
         * specified state and along-vector distance.
         */
        getLegDistanceRemaining(plan, state, vectorDistanceRemaining) {
            var _a;
            const calcs = (_a = plan.tryGetLeg(state.globalLegIndex)) === null || _a === void 0 ? void 0 : _a.calculated;
            if (!calcs) {
                return 0;
            }
            let vectors = LNavUtils.getVectorsForTransitionMode(calcs, state.transitionMode, state.isSuspended);
            const vector = vectors[state.vectorIndex];
            if (!vector) {
                return 0;
            }
            let vectorIndex = state.vectorIndex;
            let distanceRemaining = 0;
            if (state.transitionMode === exports.LNavTransitionMode.Unsuspend) {
                const lastIngressVector = calcs.ingress[calcs.ingress.length - 1];
                const ingressJoinVector = calcs.flightPath[calcs.ingressJoinIndex];
                if (ingressJoinVector && lastIngressVector) {
                    // If we are in unsuspend mode and a valid ingress transition exists, then we need to add the distance from
                    // the end of the current vector to where the ingress transition joins the base flight path.
                    for (let i = state.vectorIndex + 1; i < calcs.ingressJoinIndex; i++) {
                        distanceRemaining += vectors[i].distance;
                    }
                    // If the current vector is before the vector at which the ingress joins the base flight path, we need to
                    // add the distance from the start of the joined vector to where the ingress joins.
                    if (state.vectorIndex < calcs.ingressJoinIndex) {
                        const circle = FlightPathUtils.setGeoCircleFromVector(ingressJoinVector, this.geoCircleCache[0]);
                        const start = GeoPoint.sphericalToCartesian(ingressJoinVector.startLat, ingressJoinVector.startLon, this.vec3Cache[0]);
                        const end = GeoPoint.sphericalToCartesian(lastIngressVector.endLat, lastIngressVector.endLon, this.vec3Cache[1]);
                        distanceRemaining += UnitType.GA_RADIAN.convertTo(circle.distanceAlong(start, end, Math.PI), UnitType.METER);
                    }
                    // Reset the vector index to -1 so that we add the distance of all the ingress-to-egress vectors (it will be
                    // incremented to 0 below).
                    vectorIndex = -1;
                }
                vectors = calcs.ingressToEgress;
            }
            for (let i = vectorIndex + 1; i < vectors.length; i++) {
                distanceRemaining += vectors[i].distance;
            }
            switch (state.transitionMode) {
                case exports.LNavTransitionMode.Ingress:
                    vectors = LNavUtils.getVectorsForTransitionMode(calcs, exports.LNavTransitionMode.None, state.isSuspended);
                    for (let i = Math.max(0, state.isSuspended ? calcs.ingressJoinIndex : 0); i < vectors.length; i++) {
                        const currentVector = vectors[i];
                        if (state.isSuspended && i === calcs.ingressJoinIndex) {
                            const lastIngressVector = calcs.ingress[calcs.ingress.length - 1];
                            if (lastIngressVector) {
                                const circle = FlightPathUtils.setGeoCircleFromVector(currentVector, this.geoCircleCache[0]);
                                distanceRemaining += UnitType.GA_RADIAN.convertTo(circle.distanceAlong(this.geoPointCache[0].set(lastIngressVector.endLat, lastIngressVector.endLon), this.geoPointCache[1].set(currentVector.endLat, currentVector.endLon), Math.PI), UnitType.METER);
                                continue;
                            }
                        }
                        distanceRemaining += currentVector.distance;
                    }
                // eslint-disable-next-line no-fallthrough
                case exports.LNavTransitionMode.None:
                case exports.LNavTransitionMode.Unsuspend:
                    if (!state.isSuspended) {
                        vectors = calcs.egress;
                        for (let i = 0; i < vectors.length; i++) {
                            distanceRemaining += vectors[i].distance;
                        }
                    }
            }
            return UnitType.METER.convertTo(distanceRemaining, UnitType.NMILE) + vectorDistanceRemaining;
        }
        /**
         * Copies one LNAV state object to another.
         * @param source The LNAV state from which to copy.
         * @param target The LNAV state to which to copy.
         * @returns The target LNAV state of the copy operation.
         */
        static copyStateInfo(source, target) {
            target.globalLegIndex = source.globalLegIndex;
            target.transitionMode = source.transitionMode;
            target.vectorIndex = source.vectorIndex;
            target.isSuspended = source.isSuspended;
            target.inhibitedSuspendLegIndex = source.inhibitedSuspendLegIndex;
            target.resetVectorsOnSuspendEnd = source.resetVectorsOnSuspendEnd;
            target.isMissedApproachActive = source.isMissedApproachActive;
            return target;
        }
        /**
         * Gets the ideal bank angle, in degrees, to follow a flight path vector under conditions of no cross-track error
         * and no wind, at a given ground speed.
         * @param vector The flight path vector to follow.
         * @param groundSpeed Ground speed, in knots.
         * @returns The ideal bank angle, in degrees, to follow the specified flight path vector at the specified ground
         * speed.
         */
        static getVectorIdealBankAngle(vector, groundSpeed) {
            if (FlightPathUtils.isVectorGreatCircle(vector)) {
                return 0;
            }
            if (vector.radius < MathUtils.HALF_PI) {
                // left turn
                return NavMath.bankAngle(groundSpeed, UnitType.GA_RADIAN.convertTo(vector.radius, UnitType.METER));
            }
            else {
                // right turn
                return -NavMath.bankAngle(groundSpeed, UnitType.GA_RADIAN.convertTo(Math.PI - vector.radius, UnitType.METER));
            }
        }
    }
    LNavComputer.ANGULAR_TOLERANCE = GeoCircle.ANGULAR_TOLERANCE;
    LNavComputer.ANGULAR_TOLERANCE_METERS = UnitType.GA_RADIAN.convertTo(GeoCircle.ANGULAR_TOLERANCE, UnitType.METER);

    /**
     * SimVar names for LNAV OBS data.
     */
    exports.LNavObsVars = void 0;
    (function (LNavObsVars) {
        /** The current desired track, in degrees true. */
        LNavObsVars["Active"] = "L:WTAP_LNav_Obs_Active";
        /**
         * The current crosstrack error. Negative values indicate deviation to the left, as viewed when facing in the
         * direction of the track. Positive values indicate deviation to the right.
         */
        LNavObsVars["Course"] = "L:WTAP_LNav_Obs_Course";
    })(exports.LNavObsVars || (exports.LNavObsVars = {}));
    /**
     * A publisher for LNAV OBS events derived from SimVars.
     */
    class LNavObsSimVarPublisher extends SimVarPublisher {
        /**
         * Creates a new instance of LNavObsSimVarPublisher.
         * @param bus The event bus to which to publish.
         */
        constructor(bus) {
            const defs = ArrayUtils.flatMap([
                ['lnav_obs_active', { name: exports.LNavObsVars.Active, type: exports.SimVarValueType.Bool }],
                ['lnav_obs_course', { name: exports.LNavObsVars.Course, type: exports.SimVarValueType.Degree }],
            ], pair => {
                const [topic, entry] = pair;
                const indexedEntry = {
                    name: `${entry.name}:#index#`,
                    type: entry.type,
                    indexed: true,
                    defaultIndex: null
                };
                return [
                    pair,
                    [topic, indexedEntry]
                ];
            });
            super(defs, bus);
        }
    }

    /**
     * A manager that controls OBS state in response to control events defined in `LNavObsControlEvents`.
     */
    class LNavObsManager {
        /**
         * Creates a new instance of LNavObsManager.
         * @param bus The event bus.
         * @param index The index of the LNAV that is associated with the OBS state controlled by this manager.
         * @param useSimObsState Whether this manager uses the sim's native OBS state. If `true`, then the manager will
         * forward state changes to the sim's native OBS SimVars via key events and sync the values of the native SimVars to
         * the LVars defined in `LNavObsVars`. If `false`, then the manager will directly manipulate the LVars defined in
         * `LNavObsVars`.
         */
        constructor(bus, index, useSimObsState) {
            this.bus = bus;
            this.index = index;
            this.useSimObsState = useSimObsState;
            this.subscriptions = [];
            this.isAlive = true;
            this.isInit = false;
            if (!LNavUtils.isValidLNavIndex(index)) {
                throw new Error(`LNavObsManager: invalid index: ${index}`);
            }
            const lvarSuffix = index === 0 ? '' : `:${index}`;
            this.activeLVar = `${exports.LNavObsVars.Active}${lvarSuffix}`;
            this.courseLVar = `${exports.LNavObsVars.Course}${lvarSuffix}`;
            if (useSimObsState) {
                this.activeSimVar = 'GPS OBS ACTIVE';
                this.courseSimVar = 'GPS OBS VALUE';
            }
            else {
                this.activeSimVar = this.activeLVar;
                this.courseSimVar = this.courseLVar;
            }
        }
        /**
         * Initializes this manager. Once initialized, the manager will control OBS state in response to control events
         * received on the event bus.
         * @throws Error if this manager has been destroyed.
         */
        init() {
            if (!this.isAlive) {
                throw new Error('LNavObsManager: cannot initialize a dead manager');
            }
            if (this.isInit) {
                return;
            }
            this.isInit = true;
            const eventBusTopicSuffix = LNavUtils.getEventBusTopicSuffix(this.index);
            const sub = this.bus.getSubscriber();
            this.subscriptions.push(sub.on(`lnav_obs_set_active${eventBusTopicSuffix}`).handle(this.onSetActiveEvent.bind(this)), sub.on(`lnav_obs_toggle_active${eventBusTopicSuffix}`).handle(this.onToggleActiveEvent.bind(this)), sub.on(`lnav_obs_set_course${eventBusTopicSuffix}`).handle(this.onSetCourseEvent.bind(this)), sub.on(`lnav_obs_inc_course${eventBusTopicSuffix}`).handle(this.onIncrementCourseEvent.bind(this, 1)), sub.on(`lnav_obs_dec_course${eventBusTopicSuffix}`).handle(this.onIncrementCourseEvent.bind(this, -1)));
            if (this.useSimObsState) {
                // We need the update loop to ensure the sync happens every frame (SimVar publishing to the event bus is
                // throttled by cockpit refresh rate setting and external view, so we can't rely on data published to the bus).
                this.subscriptions.push(sub.on('simTimeHiFreq').handle(this.syncSimStateToLVars.bind(this)));
            }
        }
        /**
         * Responds to when a control event to set whether OBS is active is received.
         * @param active The state commanded by the control event.
         */
        onSetActiveEvent(active) {
            this.setActive(active);
        }
        /**
         * Responds to when a control event to toggle whether OBS is active is received.
         */
        onToggleActiveEvent() {
            if (this.useSimObsState) {
                SimVar.SetSimVarValue('K:GPS_OBS', exports.SimVarValueType.Number, 0);
            }
            else {
                this.setActive(SimVar.GetSimVarValue(this.activeSimVar, exports.SimVarValueType.Bool) === 0);
            }
        }
        /**
         * Responds to when a control event to set the OBS course is received.
         * @param course The course commanded by the control event.
         */
        onSetCourseEvent(course) {
            this.setCourse(course);
        }
        /**
         * Responds to when a control event to increment OBS course is received.
         * @param dir The direction in which to increment the OBS course.
         */
        onIncrementCourseEvent(dir) {
            if (this.useSimObsState) {
                SimVar.SetSimVarValue(dir === 1 ? 'K:GPS_OBS_INC' : 'K:GPS_OBS_DEC', exports.SimVarValueType.Number, 0);
            }
            else {
                let course = SimVar.GetSimVarValue(this.courseSimVar, exports.SimVarValueType.Degree);
                if (dir === 1) {
                    course = Math.floor(dir) + 1;
                }
                else {
                    course = Math.ceil(dir) - 1;
                }
                this.setCourse(course);
            }
        }
        /**
         * Sets whether OBS is active.
         * @param active Whether OBS should be set to active.
         */
        setActive(active) {
            if (this.useSimObsState) {
                if (active) {
                    SimVar.SetSimVarValue('K:GPS_OBS_ON', exports.SimVarValueType.Number, 0);
                }
                else {
                    SimVar.SetSimVarValue('K:GPS_OBS_OFF', exports.SimVarValueType.Number, 0);
                }
            }
            else {
                SimVar.SetSimVarValue(this.activeSimVar, exports.SimVarValueType.Bool, active ? 1 : 0);
            }
        }
        /**
         * Sets the OBS course.
         * @param course The course to set, in degrees.
         */
        setCourse(course) {
            if (!isFinite(course)) {
                return;
            }
            if (this.useSimObsState) {
                SimVar.SetSimVarValue('K:GPS_OBS_SET', exports.SimVarValueType.Number, course);
            }
            else {
                SimVar.SetSimVarValue(this.courseSimVar, exports.SimVarValueType.Degree, NavMath.normalizeHeading(course));
            }
        }
        /**
         * Syncs the sim's native OBS state to the LVars controlled by this manager.
         */
        syncSimStateToLVars() {
            SimVar.SetSimVarValue(this.activeLVar, exports.SimVarValueType.Bool, SimVar.GetSimVarValue('GPS OBS ACTIVE', exports.SimVarValueType.Bool));
            SimVar.SetSimVarValue(this.courseLVar, exports.SimVarValueType.Degree, SimVar.GetSimVarValue('GPS OBS VALUE', exports.SimVarValueType.Degree));
        }
        /**
         * Destroys this manager. Once destroyed, this manager will no longer control OBS state.
         */
        destroy() {
            this.isAlive = false;
            for (const sub of this.subscriptions) {
                sub.destroy();
            }
        }
    }

    /**
     * Types of input acceleration supported by {@link AltitudeSelectManager}.
     */
    exports.AltitudeSelectManagerAccelType = void 0;
    (function (AltitudeSelectManagerAccelType) {
        /** No input acceleration. */
        AltitudeSelectManagerAccelType["None"] = "None";
        /** While input acceleration is active, small-increment inputs will be converted to large increments. */
        AltitudeSelectManagerAccelType["SmallToLarge"] = "SmallToLarge";
        /**
         * While input acceleration is active, the magnitude of small-increment inputs will be adjusted by an arbitrary
         * factor based on the observed rate of small-increment inputs.
         */
        AltitudeSelectManagerAccelType["DynamicSmall"] = "DynamicSmall";
    })(exports.AltitudeSelectManagerAccelType || (exports.AltitudeSelectManagerAccelType = {}));
    /**
     * Bitflags used to filter input events eligible to trigger input acceleration for {@link AltitudeSelectManager}.
     */
    exports.AltitudeSelectManagerAccelFilter = void 0;
    (function (AltitudeSelectManagerAccelFilter) {
        /** No events. */
        AltitudeSelectManagerAccelFilter[AltitudeSelectManagerAccelFilter["None"] = 0] = "None";
        /** INC/DEC events with a value of zero. */
        AltitudeSelectManagerAccelFilter[AltitudeSelectManagerAccelFilter["ZeroIncDec"] = 1] = "ZeroIncDec";
        /** INC/DEC events with a non-zero value. */
        AltitudeSelectManagerAccelFilter[AltitudeSelectManagerAccelFilter["NonZeroIncDec"] = 2] = "NonZeroIncDec";
        /** SET events that are transformed to an INC/DEC event. */
        AltitudeSelectManagerAccelFilter[AltitudeSelectManagerAccelFilter["TransformedSet"] = 4] = "TransformedSet";
        /** All events. */
        AltitudeSelectManagerAccelFilter[AltitudeSelectManagerAccelFilter["All"] = -1] = "All";
    })(exports.AltitudeSelectManagerAccelFilter || (exports.AltitudeSelectManagerAccelFilter = {}));
    /**
     * Controls the value of the autopilot selected altitude setting in response to key events.
     */
    class AltitudeSelectManager {
        /**
         * Creates a new instance of AltitudeSelectManager.
         * @param bus The event bus.
         * @param settingsManager The user settings manager controlling metric altitude preselector setting. Required to
         * support metric mode.
         * @param options Configuration options for this manager.
         * @param stops Additional altitude stops, in feet, to respect when the selected altitude is incremented or
         * decremented.
         */
        constructor(bus, settingsManager, options, stops) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.stops = new SortedArray((a, b) => a - b);
            this.lockDebounceTimer = new DebounceTimer();
            this.isKeyEventInit = false;
            this.isEnabled = true;
            this.isInitialized = false;
            this.isPaused = false;
            this.isLocked = false;
            this.frameCounter = 0;
            this.isAccelActive = false;
            this.accelConsecIncrSmallCount = 0;
            this.accelLastIncrDirection = 0;
            this.accelLastIncrInputTime = 0;
            this.accelWindowRecord = [];
            this.lastAccelInputFrame = undefined;
            this.accelInputRateResidual = 0;
            this.selectedAltitudeChangedHandler = () => {
                // wait one frame before unlocking due to delay between when a key event is created and when it is intercepted on
                // the JS side
                setTimeout(() => {
                    this.isLocked = false;
                    this.lockDebounceTimer.clear();
                });
            };
            this.altitudeHoldSlotIndex = (_a = options.altitudeHoldSlotIndex) !== null && _a !== void 0 ? _a : 1;
            this.altitudeHoldSlotSimVar = `AUTOPILOT ALTITUDE LOCK VAR:${this.altitudeHoldSlotIndex}`;
            this.minValue = Math.round(options.minValue.asUnit(UnitType.FOOT));
            this.maxValue = Math.round(options.maxValue.asUnit(UnitType.FOOT));
            this.minValueMetric = Math.round(((_b = options.minValueMetric) !== null && _b !== void 0 ? _b : options.minValue).asUnit(UnitType.METER));
            this.maxValueMetric = Math.round(((_c = options.maxValueMetric) !== null && _c !== void 0 ? _c : options.maxValue).asUnit(UnitType.METER));
            this.inputIncrLargeThreshold = options.inputIncrLargeThreshold;
            this.incrSmall = Math.round(options.incrSmall.asUnit(UnitType.FOOT));
            this.incrLarge = Math.round(options.incrLarge.asUnit(UnitType.FOOT));
            this.incrSmallMetric = Math.round(((_d = options.incrSmallMetric) !== null && _d !== void 0 ? _d : options.incrSmall).asUnit(UnitType.METER));
            this.incrLargeMetric = Math.round(((_e = options.incrLargeMetric) !== null && _e !== void 0 ? _e : options.incrLarge).asUnit(UnitType.METER));
            this.lockAltToStepOnIncr = (_f = options.lockAltToStepOnIncr) !== null && _f !== void 0 ? _f : true;
            this.lockAltToStepOnIncrMetric = (_g = options.lockAltToStepOnIncrMetric) !== null && _g !== void 0 ? _g : this.lockAltToStepOnIncr;
            this.lockAltToStepOnSet = (_h = options.lockAltToStepOnSet) !== null && _h !== void 0 ? _h : false;
            this.lockAltToStepOnSetMetric = (_j = options.lockAltToStepOnSetMetric) !== null && _j !== void 0 ? _j : this.lockAltToStepOnSet;
            this.accelType = (_k = options.accelType) !== null && _k !== void 0 ? _k : exports.AltitudeSelectManagerAccelType.None;
            this.accelInputCountThreshold = (_l = options.accelInputCountThreshold) !== null && _l !== void 0 ? _l : 0;
            this.accelInputCountWindow = (_m = options.accelInputCountWindow) !== null && _m !== void 0 ? _m : 300;
            this.accelInputRateThreshold = (_o = options.accelInputRateThreshold) !== null && _o !== void 0 ? _o : 0;
            this.accelInputRateWindow = (_p = options.accelInputRateWindow) !== null && _p !== void 0 ? _p : 0;
            this.accelInputRateTransformer = (_q = options.accelInputRateTransformer) !== null && _q !== void 0 ? _q : AltitudeSelectManager.defaultInputRateTransformer;
            this.accelResetOnDirectionChange = (_r = options.accelResetOnDirectionChange) !== null && _r !== void 0 ? _r : false;
            this.accelFilter = (_s = options.accelFilter) !== null && _s !== void 0 ? _s : exports.AltitudeSelectManagerAccelFilter.All;
            this.initToIndicatedAlt = (_t = options.initToIndicatedAlt) !== null && _t !== void 0 ? _t : false;
            this.transformSetToIncDec = (_u = options.transformSetToIncDec) !== null && _u !== void 0 ? _u : true;
            this.altimeterMetricSetting = options.supportMetric && settingsManager ? settingsManager.getSetting('altMetric') : undefined;
            if (stops !== undefined) {
                if ('isSubscribableSet' in stops) {
                    stops.sub(this.onStopsChanged.bind(this), true);
                }
                else {
                    this.stops.insertAll(new Set(stops)); // Make sure there are no duplicates.
                }
            }
            this.isInitialized = !((_v = options.initOnInput) !== null && _v !== void 0 ? _v : false);
            this.initKeyEvents();
        }
        /**
         * Initializes this manager's key event intercepts.
         */
        async initKeyEvents() {
            this.keyEventManager = await KeyEventManager.getManager(this.bus);
            this.keyEventManager.interceptKey('AP_ALT_VAR_SET_ENGLISH', false);
            this.keyEventManager.interceptKey('AP_ALT_VAR_SET_METRIC', false);
            this.keyEventManager.interceptKey('AP_ALT_VAR_INC', false);
            this.keyEventManager.interceptKey('AP_ALT_VAR_DEC', false);
            this.isKeyEventInit = true;
            const sub = this.bus.getSubscriber();
            if (this.transformSetToIncDec) {
                sub.on(`ap_altitude_selected_${this.altitudeHoldSlotIndex}`).whenChanged().handle(this.selectedAltitudeChangedHandler);
            }
            sub.on('key_intercept').handle(this.onKeyIntercepted.bind(this));
            this.publisher.pub('alt_select_is_initialized', !this.isEnabled || this.isInitialized, true);
            sub.on('simTimeHiFreq').handle(this.update.bind(this));
        }
        /**
         * Sets whether this manager is enabled. When this manager is disabled, all key events to change the selected
         * altitude setting are processed "as-is".
         * @param isEnabled Whether this manager is enabled.
         */
        setEnabled(isEnabled) {
            if (this.isEnabled === isEnabled) {
                return;
            }
            this.isEnabled = isEnabled;
            if (this.isKeyEventInit) {
                this.publisher.pub('alt_select_is_initialized', !isEnabled || this.isInitialized, true);
            }
        }
        /**
         * Resumes this manager. When resumed, this manager will respond to key events that manipulate selected altitude.
         */
        resume() {
            this.isPaused = false;
        }
        /**
         * Pauses this manager. When paused, this manager will not respond to key events that manipulate selected altitude.
         * If this manager is disabled, it will still pass through key events while paused.
         */
        pause() {
            this.isPaused = true;
        }
        /**
         * Resets the selected altitude to a specific value and optionally sets the initialized state of the selected
         * altitude to uninitialized.
         * @param altitude The altitude, in feet, to which to reset the selected altitude.
         * @param resetInitialized Whether to reset the initialized state of the selected altitude to uninitialized. Defaults
         * to `false`.
         */
        reset(altitude, resetInitialized = false) {
            if (!this.isEnabled) {
                return;
            }
            SimVar.SetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet, altitude);
            if (resetInitialized) {
                this.isInitialized = false;
                if (this.isKeyEventInit) {
                    this.publisher.pub('alt_select_is_initialized', false, true);
                }
            }
        }
        /**
         * Sets the initialized state of the selected altitude.
         * @param initialized The state to set.
         */
        setSelectedAltitudeInitialized(initialized) {
            if (this.isInitialized === initialized) {
                return;
            }
            this.isInitialized = initialized;
            if (this.isKeyEventInit) {
                this.publisher.pub('alt_select_is_initialized', !this.isEnabled || initialized, true);
            }
        }
        /**
         * Responds to when the set of additional altitude increment stops changes.
         * @param set The set containing all additional altitude increment stops.
         * @param type The type of change.
         * @param stop The stop that changed.
         */
        onStopsChanged(set, type, stop) {
            if (type === exports.SubscribableSetEventType.Added) {
                this.stops.insert(stop);
            }
            else {
                this.stops.remove(stop);
            }
        }
        /**
         * Responds to key intercepted events.
         * @param data The event data.
         * @param data.key The key that was intercepted.
         * @param data.value0 The value of the intercepted key event.
         * @param data.value1 The index of the intercepted key event.
         */
        onKeyIntercepted({ key, value0: value, value1: index }) {
            switch (key) {
                case 'AP_ALT_VAR_INC':
                case 'AP_ALT_VAR_DEC':
                case 'AP_ALT_VAR_SET_ENGLISH':
                case 'AP_ALT_VAR_SET_METRIC':
                    break;
                default:
                    return;
            }
            index !== null && index !== void 0 ? index : (index = 1); // key events without an explicit index automatically get mapped to index 1
            index = Math.max(1, index); // treat index 0 events as index 1.
            if (!this.isEnabled || index !== this.altitudeHoldSlotIndex) {
                this.passThroughKeyEvent(key, index, value);
                return;
            }
            if (!this.isPaused && !this.isLocked) {
                this.handleKeyEvent(key, value);
            }
        }
        /**
         * Handles a key event.
         * @param key The key.
         * @param value The value of the key event.
         */
        handleKeyEvent(key, value) {
            const currentValue = SimVar.GetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet);
            let startValue = currentValue;
            if (!this.isInitialized) {
                if (this.initToIndicatedAlt) {
                    startValue = SimVar.GetSimVarValue('INDICATED ALTITUDE', exports.SimVarValueType.Feet);
                }
                else {
                    startValue = 0;
                }
            }
            let direction = 0;
            let useLargeIncrement = false;
            let setAltitude = undefined;
            let accelFilterChallenge = 0;
            switch (key) {
                case 'AP_ALT_VAR_INC':
                    direction = 1;
                    useLargeIncrement = value !== undefined && value > this.inputIncrLargeThreshold;
                    if (value === undefined || value === 0) {
                        accelFilterChallenge = exports.AltitudeSelectManagerAccelFilter.ZeroIncDec;
                    }
                    else {
                        accelFilterChallenge = exports.AltitudeSelectManagerAccelFilter.NonZeroIncDec;
                    }
                    break;
                case 'AP_ALT_VAR_DEC':
                    direction = -1;
                    useLargeIncrement = value !== undefined && value > this.inputIncrLargeThreshold;
                    if (value === undefined || value === 0) {
                        accelFilterChallenge = exports.AltitudeSelectManagerAccelFilter.ZeroIncDec;
                    }
                    else {
                        accelFilterChallenge = exports.AltitudeSelectManagerAccelFilter.NonZeroIncDec;
                    }
                    break;
                case 'AP_ALT_VAR_SET_ENGLISH':
                case 'AP_ALT_VAR_SET_METRIC': {
                    if (value !== undefined && value !== currentValue) {
                        if (this.transformSetToIncDec) {
                            const delta = value - currentValue;
                            direction = delta < 0 ? -1 : 1;
                            useLargeIncrement = Math.abs(delta) > this.inputIncrLargeThreshold;
                            accelFilterChallenge = exports.AltitudeSelectManagerAccelFilter.TransformedSet;
                        }
                        else {
                            setAltitude = value;
                        }
                    }
                    break;
                }
            }
            if (setAltitude !== undefined) {
                this.setSelectedAltitude(setAltitude);
                return;
            }
            // Handle input acceleration
            switch (this.accelType) {
                case exports.AltitudeSelectManagerAccelType.SmallToLarge:
                    this.handleSmallToLargeAccel(startValue, direction, useLargeIncrement, accelFilterChallenge);
                    break;
                case exports.AltitudeSelectManagerAccelType.DynamicSmall:
                    this.handleDynamicSmallAccel(startValue, direction, useLargeIncrement, accelFilterChallenge);
                    break;
                default:
                    if (direction !== 0) {
                        this.changeSelectedAltitude(startValue, direction, useLargeIncrement);
                    }
            }
        }
        /**
         * Sets the selected altitude to a specific value.
         * @param altitudeFeet The altitude to set, in feet.
         */
        setSelectedAltitude(altitudeFeet) {
            var _a, _b;
            const isMetric = (_b = (_a = this.altimeterMetricSetting) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : false;
            let lockIncr;
            let min, max, unit;
            if (isMetric) {
                min = this.minValueMetric;
                max = this.maxValueMetric;
                unit = UnitType.METER;
                lockIncr = this.lockAltToStepOnSetMetric ? this.incrSmallMetric : 0;
            }
            else {
                min = this.minValue;
                max = this.maxValue;
                unit = UnitType.FOOT;
                lockIncr = this.lockAltToStepOnSet ? this.incrSmall : 0;
            }
            let altitudeInUnits = UnitType.FOOT.convertTo(altitudeFeet, unit);
            if (lockIncr > 0) {
                altitudeInUnits = MathUtils.clamp(MathUtils.round(altitudeInUnits, lockIncr), MathUtils.ceil(min, lockIncr), MathUtils.floor(max, lockIncr));
            }
            else {
                altitudeInUnits = MathUtils.clamp(altitudeInUnits, min, max);
            }
            const valueToSet = UnitType.FOOT.convertFrom(altitudeInUnits, unit);
            if (valueToSet !== SimVar.GetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet)) {
                SimVar.SetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet, valueToSet);
            }
            if (!this.isInitialized) {
                this.publisher.pub('alt_select_is_initialized', true, true, true);
                this.isInitialized = true;
            }
        }
        /**
         * Handles an increment/decrement input with small-to-large increment acceleration.
         * @param startValue The value from which to change, in feet.
         * @param direction The direction of change.
         * @param useLargeIncrement Whether the input to handle is for a large increment.
         * @param accelFilterChallenge Bitflags with which to challenge this manager's input acceleration filter. The input
         * will meet the conditions for acceleration if and only if the challenge flags shares at least one non-zero bit with
         * the filter.
         */
        handleSmallToLargeAccel(startValue, direction, useLargeIncrement, accelFilterChallenge) {
            if (this.accelInputCountThreshold > 0) {
                const time = Date.now();
                let isAccelActive = this.accelConsecIncrSmallCount >= this.accelInputCountThreshold;
                const didPassFilter = BitFlags.isAny(accelFilterChallenge, this.accelFilter);
                if (useLargeIncrement
                    || !didPassFilter
                    || direction === 0
                    || (this.accelConsecIncrSmallCount > 0 && time - this.accelLastIncrInputTime > this.accelInputCountWindow)
                    || ((isAccelActive ? this.accelResetOnDirectionChange : this.accelConsecIncrSmallCount > 0) && this.accelLastIncrDirection !== direction)) {
                    this.accelConsecIncrSmallCount = 0;
                }
                if (!useLargeIncrement && didPassFilter) {
                    this.accelConsecIncrSmallCount++;
                    this.accelLastIncrDirection = direction;
                    this.accelLastIncrInputTime = time;
                }
                isAccelActive = this.accelConsecIncrSmallCount >= this.accelInputCountThreshold;
                if (isAccelActive) {
                    useLargeIncrement = true;
                }
                this.isAccelActive = isAccelActive;
            }
            if (direction !== 0) {
                this.changeSelectedAltitude(startValue, direction, useLargeIncrement);
            }
        }
        /**
         * Handles an increment/decrement input with dynamic small-increment acceleration.
         * @param startValue The value from which to change, in feet.
         * @param direction The direction of change.
         * @param useLargeIncrement Whether the input to handle is for a large increment.
         * @param accelFilterChallenge Bitflags with which to challenge this manager's input acceleration filter. The input
         * will meet the conditions for acceleration if and only if the challenge flags shares at least one non-zero bit with
         * the filter.
         */
        handleDynamicSmallAccel(startValue, direction, useLargeIncrement, accelFilterChallenge) {
            let delta = direction;
            if (this.accelInputRateWindow > 0) {
                const time = Date.now();
                if (direction !== 0 && direction !== this.accelLastIncrDirection) {
                    if (this.isAccelActive) {
                        this.accelInputRateResidual = 0;
                        if (this.accelResetOnDirectionChange) {
                            this.accelWindowRecord.length = 0;
                            this.isAccelActive = false;
                        }
                    }
                    else {
                        this.accelWindowRecord.length = 0;
                    }
                }
                this.updateAccelWindowRecord(time);
                const isLastAccelInputThisFrame = this.frameCounter === this.lastAccelInputFrame;
                const isInputEligibleForAccel = direction !== 0 && !useLargeIncrement && BitFlags.isAny(accelFilterChallenge, this.accelFilter);
                // Do not push the current input into the record if the input was received on the same frame as the previous
                // acceleration-eligible input. We are effectively limited to changing the selected altitude once per frame, so
                // we are unable to "accumulate" multiple increments per frame.
                if (isInputEligibleForAccel && !isLastAccelInputThisFrame) {
                    this.accelWindowRecord.push(time);
                    this.lastAccelInputFrame = this.frameCounter;
                }
                const inputRate = this.accelWindowRecord.length / this.accelInputRateWindow * 1000;
                const isAccelActive = inputRate >= this.accelInputRateThreshold;
                if (isAccelActive !== this.isAccelActive) {
                    if (!isAccelActive) {
                        this.accelWindowRecord.length = 0;
                        this.accelInputRateResidual = 0;
                    }
                    this.isAccelActive = isAccelActive;
                }
                else if (isAccelActive && isInputEligibleForAccel) {
                    // Do not try to change the selected altitude if the current input was received on the same frame as the
                    // previous acceleration-eligible input.
                    if (isLastAccelInputThisFrame) {
                        delta = 0;
                    }
                    else {
                        // Over multiple frames, error from quantizing the change in the selected altitude to the small increment can
                        // add to become significant. To combat this, we calculate a residual input rate and add it to the next
                        // accelerated input.
                        const desiredInputRate = Math.max(Math.max(this.accelInputRateTransformer(inputRate), 0) + this.accelInputRateResidual, 0);
                        const increment = Math.round(desiredInputRate / inputRate);
                        this.accelInputRateResidual = desiredInputRate - increment * inputRate;
                        delta *= increment;
                    }
                }
                this.accelLastIncrDirection = direction;
            }
            if (delta !== 0) {
                this.changeSelectedAltitude(startValue, delta, useLargeIncrement);
            }
        }
        /**
         * Updates the record of input events within the acceleration window. Existing events in the record that are older
         * than the window are removed. If the current time is less than the most recent event in the record, then all events
         * are removed from the record.
         * @param time The current real (operating system) time, as a Javascript timestamp.
         */
        updateAccelWindowRecord(time) {
            if (this.accelWindowRecord.length === 0) {
                return;
            }
            // Check if the most recent time is after the current time (can only happen if the operating system clock is
            // changed). If it is, then empty the entire record.
            if (this.accelWindowRecord[this.accelWindowRecord.length - 1] > time) {
                this.accelWindowRecord.length = 0;
            }
            // Remove all entries that are older than the window.
            let startIndex = 0;
            for (; startIndex < this.accelWindowRecord.length; startIndex++) {
                const dt = time - this.accelWindowRecord[startIndex];
                if (dt <= this.accelInputRateWindow) {
                    break;
                }
            }
            this.accelWindowRecord.splice(0, startIndex);
        }
        /**
         * Increments or decrements the selected altitude setting. The amount the setting is changed depends on whether the
         * PFD altimeter metric mode is enabled. The value of the setting after the change is guaranteed to be a round number
         * in the appropriate units (nearest 100 feet or 50 meters).
         * @param startValue The value from which to change, in feet.
         * @param delta The direction of the change: `1` for increment, `-1` for decrement.
         * @param useLargeIncrement Whether to change the altitude by the large increment (1000 feet/500 meters) instead of
         * the small increment (100 feet/50 meters). False by default.
         */
        changeSelectedAltitude(startValue, delta, useLargeIncrement = false) {
            var _a, _b;
            if (delta === 0) {
                return;
            }
            const roundFunc = delta > 0 ? MathUtils.floor : MathUtils.ceil;
            const isMetric = (_b = (_a = this.altimeterMetricSetting) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : false;
            let min, max, incrSmall, incrLarge, units, lockAlt;
            if (isMetric) {
                min = this.minValueMetric;
                max = this.maxValueMetric;
                incrSmall = this.incrSmallMetric;
                incrLarge = this.incrLargeMetric;
                units = UnitType.METER;
                lockAlt = this.lockAltToStepOnIncrMetric;
            }
            else {
                min = this.minValue;
                max = this.maxValue;
                incrSmall = this.incrSmall;
                incrLarge = this.incrLarge;
                units = UnitType.FOOT;
                lockAlt = this.lockAltToStepOnIncr;
            }
            const startValueConverted = Math.round(UnitType.FOOT.convertTo(startValue, units));
            useLargeIncrement && (useLargeIncrement = !lockAlt || (startValueConverted % incrSmall === 0));
            let valueToSet = UnitType.FOOT.convertFrom(MathUtils.clamp((lockAlt ? roundFunc(startValueConverted, incrSmall) : startValueConverted) + delta * (useLargeIncrement ? incrLarge : incrSmall), min, max), units);
            // Check if we need to set the new altitude to a stop instead.
            if (this.stops.length > 0) {
                let nextStopIndex = this.stops.matchIndex(startValue);
                if (delta > 0) {
                    if (nextStopIndex < 0) {
                        nextStopIndex = -nextStopIndex - 1;
                    }
                    else {
                        nextStopIndex++;
                    }
                }
                else {
                    if (nextStopIndex < 0) {
                        nextStopIndex = -nextStopIndex - 2;
                    }
                    else {
                        nextStopIndex--;
                    }
                }
                const nextStop = this.stops.peek(nextStopIndex);
                if (nextStop !== undefined && Math.abs(valueToSet - startValue) > Math.abs(nextStop - startValue)) {
                    valueToSet = nextStop;
                }
            }
            if (valueToSet !== SimVar.GetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet)) {
                SimVar.SetSimVarValue(this.altitudeHoldSlotSimVar, exports.SimVarValueType.Feet, valueToSet);
                // If we are transforming SET events to INC/DEC, we need to lock out any further changes until the simvar is
                // completely updated. Otherwise, calculations of the inc/dec delta that rely on knowing the current value of
                // the simvar will be incorrect.
                if (this.transformSetToIncDec) {
                    this.isLocked = true;
                    // Sometimes the alt select change event will not fire if the change is too small, so we set a timeout to unlock
                    // just in case
                    this.lockDebounceTimer.schedule(() => { this.isLocked = false; }, 250);
                }
            }
            if (!this.isInitialized) {
                this.publisher.pub('alt_select_is_initialized', true, true, true);
                this.isInitialized = true;
            }
        }
        /**
         * Processes a key event "as-is".
         * @param key The key that was pressed.
         * @param index The index of the key event.
         * @param value The value of the key event.
         */
        passThroughKeyEvent(key, index, value) {
            index = Math.max(1, index);
            const currentValue = SimVar.GetSimVarValue(`AUTOPILOT ALTITUDE LOCK VAR:${index}`, exports.SimVarValueType.Feet);
            let valueToSet = currentValue;
            switch (key) {
                case 'AP_ALT_VAR_SET_ENGLISH':
                case 'AP_ALT_VAR_SET_METRIC':
                    if (value !== undefined) {
                        valueToSet = value;
                    }
                    break;
                case 'AP_ALT_VAR_INC':
                    valueToSet += value === 0 || value === undefined ? 100 : value;
                    break;
                case 'AP_ALT_VAR_DEC':
                    valueToSet -= value === 0 || value === undefined ? 100 : value;
                    break;
            }
            SimVar.SetSimVarValue(`AUTOPILOT ALTITUDE LOCK VAR:${index}`, exports.SimVarValueType.Feet, valueToSet);
        }
        /**
         * Updates this manager every frame.
         */
        update() {
            this.frameCounter = (this.frameCounter + 1) % Number.MAX_SAFE_INTEGER;
        }
        /**
         * A function that returns an input rate without changing it.
         * @param inputRate An input rate.
         * @returns The specified input rate, unchanged.
         */
        static defaultInputRateTransformer(inputRate) {
            return inputRate;
        }
    }

    /** AP Mode Types */
    exports.APModeType = void 0;
    (function (APModeType) {
        APModeType[APModeType["LATERAL"] = 0] = "LATERAL";
        APModeType[APModeType["VERTICAL"] = 1] = "VERTICAL";
        APModeType[APModeType["APPROACH"] = 2] = "APPROACH";
    })(exports.APModeType || (exports.APModeType = {}));
    /**
     * A class that manages the autopilot modes and autopilot mode states.
     */
    class APStateManager {
        /**
         * Creates an instance of the APStateManager.
         * @param bus An instance of the event bus.
         * @param apConfig This autopilot's configuration.
         */
        constructor(bus, apConfig) {
            this.bus = bus;
            this.apConfig = apConfig;
            this.apListenerRegistered = false;
            this.flightDirectorStateSimVars = {
                [1]: 'AUTOPILOT FLIGHT DIRECTOR ACTIVE:1',
                [2]: 'AUTOPILOT FLIGHT DIRECTOR ACTIVE:2',
            };
            this.flightDirectorSimStates = {
                [1]: undefined,
                [2]: undefined,
            };
            this.flightDirectorPendingStates = {
                [1]: false,
                [2]: false,
            };
            this.flightDirectorPushedPendingStates = {
                [1]: false,
                [2]: false,
            };
            this._stateManagerInitialized = Subject.create(false);
            /** Whether this manager has been initialized. */
            this.stateManagerInitialized = this._stateManagerInitialized;
            this.lateralPressed = new SubEvent();
            this.verticalPressed = new SubEvent();
            this.approachPressed = new SubEvent();
            this.vnavPressed = new SubEvent();
            this.apMasterOn = Subject.create(false);
            this._isFlightDirectorOn = Subject.create(false);
            this.isFlightDirectorOn = this._isFlightDirectorOn;
            this._isFlightDirectorCoPilotOn = Subject.create(false);
            this.isFlightDirectorCoPilotOn = this._isFlightDirectorCoPilotOn;
            /** Whether any flight director is switched on. */
            this.isAnyFlightDirectorOn = MappedSubject.create(SubscribableMapFunctions.or(), this._isFlightDirectorOn, this._isFlightDirectorCoPilotOn);
            this.apListenerPromise = this.initAPListener();
            this.keyEventManagerPromise = this.initKeyEventManager();
        }
        /**
         * Initializes the Coherent autopilot listener and calls `this.onAPListenerRegistered()` when the listener is
         * registered.
         * @returns A Promise which is fulfilled when the autopilot listener has been registered and
         * `this.onAPListenerRegistered()` is finished executing.
         */
        initAPListener() {
            return new Promise(resolve => {
                RegisterViewListener('JS_LISTENER_AUTOPILOT', () => {
                    this.apListenerRegistered = true;
                    this.onAPListenerRegistered();
                    resolve();
                });
            });
        }
        /**
         * Initializes the key event manager and calls `this.onKeyEventManagerReady()` when the manager has been retrieved.
         * @returns A Promise which is fulfilled with the key event manager when it has been retrieved and
         * `this.onKeyEventManagerReady()` is finished executing.
         */
        initKeyEventManager() {
            return new Promise(resolve => {
                KeyEventManager.getManager(this.bus).then(manager => {
                    this.keyEventManager = manager;
                    this.onKeyEventManagerReady(manager);
                    resolve(manager);
                });
            });
        }
        /**
         * Waits until the Coherent autopilot listener has been registered and `this.onAPListenerRegistered()` has finished
         * executing.
         * @returns A Promise which is fulfilled when the Coherent autopilot listener has been registered and
         * `this.onAPListenerRegistered()` has finished executing.
         */
        awaitApListenerRegistered() {
            return this.apListenerPromise;
        }
        /**
         * Waits until the key event manager has been retrieved and `this.onKeyEventManagerReady()` has finished executing.
         * @returns A Promise which is fulfilled when the key event manager has been retrieved and
         * `this.onKeyEventManagerReady()` has finished executing.
         */
        awaitKeyEventManagerReady() {
            return this.keyEventManagerPromise;
        }
        /**
         * A callback which is called when the Coherent autopilot listener has been registered.
         */
        onAPListenerRegistered() {
            const ap = this.bus.getSubscriber();
            ap.on('ap_lock_set').handle(lock => {
                if (lock === exports.APLockType.VNav) {
                    this.vnavPressed.notify(this, true);
                }
            });
            ap.on('ap_lock_release').handle(lock => {
                if (lock === exports.APLockType.VNav) {
                    this.vnavPressed.notify(this, false);
                }
            });
            ap.on('ap_master_status').handle(this.apMasterOn.set.bind(this.apMasterOn));
        }
        /**
         * A callback which is called when the key event manager has been retrieved.
         * @param manager The key event manager.
         */
        onKeyEventManagerReady(manager) {
            this.setupKeyIntercepts(manager);
            this.bus.getSubscriber().on('key_intercept').handle(this.handleKeyIntercepted.bind(this));
        }
        /**
         * Sets up key intercepts necessary for managing the sim's flight director state.
         * @param manager The key event manager.
         */
        setupFlightDirectorKeyIntercepts(manager) {
            manager.interceptKey('TOGGLE_FLIGHT_DIRECTOR', false);
        }
        /**
         * Initializes this manager. If this manager has already been initialized, then this method does nothing.
         * @returns A Promise which will be fulfilled when the manager has been initialized. If the manager has already been
         * initialized, then the Promise will be fulfilled immediately.
         */
        async initialize() {
            if (this._stateManagerInitialized.get()) {
                return;
            }
            this.onBeforeInitialize();
            await Promise.all([this.awaitApListenerRegistered(), this.awaitKeyEventManagerReady()]);
            await this.enableAvionicsManagedMode();
            this.updateFlightDirectorStateFromSim(1);
            this.updateFlightDirectorStateFromSim(2);
            this.initFlightDirector();
            this._stateManagerInitialized.set(true);
        }
        /**
         * Enables avionics managed mode for the autopilot.
         */
        async enableAvionicsManagedMode() {
            if (!this.apListenerRegistered) {
                await this.awaitApListenerRegistered();
            }
            await Coherent.call('apSetAutopilotMode', exports.MSFSAPStates.AvionicsManaged, 1);
        }
        /**
         * Initializes the flight director to a default value.
         */
        initFlightDirector() {
            this.setFlightDirector(false);
        }
        /**
         * Sets the flight director state.
         * @param state The state to set: `true` = on, `false` = off.
         * @param index The index of the flight director to set. If not defined, then the state of both flight directors will
         * be set. This parameter is ignored if the autopilot is not configured with independent flight directors, in which
         * case the state of both flight directors will always be set.
         */
        setFlightDirector(state, index) {
            if (!this.apConfig.independentFds) {
                index = undefined;
            }
            if (index === 1 || index === undefined) {
                this.setFlightDirectorState(1, state);
            }
            if (index === 2 || index === undefined) {
                this.setFlightDirectorState(2, state);
            }
        }
        /**
         * Sets the state of a flight director.
         * @param index The index of the flight director to set.
         * @param state The state to set.
         */
        setFlightDirectorState(index, state) {
            this.pendSimFlightDirectorState(index, state);
        }
        /**
         * Pends a flight director state to push to the sim.
         * @param index The index of the flight director for which to pend the state.
         * @param state The state to pend.
         */
        pendSimFlightDirectorState(index, state) {
            this.flightDirectorPendingStates[index] = state;
        }
        /**
         * A method that is called on every autopilot update cycle before the autopilot directors are updated.
         */
        onBeforeUpdate() {
            this.updateFlightDirectorStateFromSim(1);
            this.updateFlightDirectorStateFromSim(2);
        }
        /**
         * Updates this manager's tracked flight director state from the sim.
         * @param index The index of the flight director to update.
         */
        updateFlightDirectorStateFromSim(index) {
            const simState = SimVar.GetSimVarValue(this.flightDirectorStateSimVars[1], exports.SimVarValueType.Bool) !== 0;
            if (simState !== this.flightDirectorSimStates[index]) {
                this.flightDirectorSimStates[index] = simState;
                this.flightDirectorPendingStates[index] = simState;
                this.flightDirectorPushedPendingStates[index] = simState;
                this.onFlightDirectorSimStateChanged(index, simState);
            }
        }
        /**
         * Responds to when the sim state of a flight director changes.
         * @param index The index of the flight director whose state changed.
         * @param state The flight director's new state.
         */
        onFlightDirectorSimStateChanged(index, state) {
            if (!this.apConfig.independentFds) {
                this.setFlightDirectorState(index === 1 ? 2 : 1, state);
            }
            if (index === 1) {
                this._isFlightDirectorOn.set(state);
            }
            else {
                this._isFlightDirectorCoPilotOn.set(state);
            }
        }
        /**
         * A method that is called on every autopilot update cycle after the autopilot directors are updated.
         */
        onAfterUpdate() {
            this.pushPendingFlightDirectorStateToSim(1);
            this.pushPendingFlightDirectorStateToSim(2);
        }
        /**
         * Pushes the pending state of a flight director to the sim.
         * @param index The index of the flight director for which to push the pending state.
         */
        pushPendingFlightDirectorStateToSim(index) {
            if (this.flightDirectorPendingStates[index] !== this.flightDirectorPushedPendingStates[index]) {
                SimVar.SetSimVarValue('K:TOGGLE_FLIGHT_DIRECTOR', 'number', index);
                this.flightDirectorPushedPendingStates[index] = this.flightDirectorPendingStates[index];
            }
        }
        /**
         * Toggles VNAV L Var value.
         */
        toggleVnav() {
            const vnavXmlVarValue = SimVar.GetSimVarValue('L:XMLVAR_VNAVButtonValue', 'Bool');
            SimVar.SetSimVarValue('L:XMLVAR_VNAVButtonValue', 'Bool', vnavXmlVarValue ? 0 : 1);
        }
        /**
         * Sends AP Mode Events from the Intercept to the Autopilot.
         * @param type is the AP Mode Type for this event
         * @param mode is the mode to set/unset.
         * @param set is whether to actively set or unset this mode.
         */
        sendApModeEvent(type, mode, set) {
            switch (type) {
                case exports.APModeType.LATERAL:
                    if (mode !== undefined) {
                        this.lateralPressed.notify(this, { mode: mode, set: set });
                    }
                    break;
                case exports.APModeType.VERTICAL:
                    if (mode !== undefined) {
                        this.verticalPressed.notify(this, { mode: mode, set: set });
                    }
                    break;
                case exports.APModeType.APPROACH:
                    this.approachPressed.notify(this, set);
                    break;
            }
        }
        /**
         * Method to override with steps to run before initialze method is run.
         */
        onBeforeInitialize() {
            //noop
        }
    }

    /**
     * A publisher for events used to control VNAV.
     */
    class VNavControlEventPublisher extends BasePublisher {
        /**
         * Creates a new instance of VNavControlEventPublisher.
         * @param bus The event bus to which to publish.
         */
        constructor(bus) {
            super(bus);
        }
        /**
         * Publish a VNav Control event.
         * @param event The event from ControlEvents.
         * @param value The value of the event.
         */
        publishEvent(event, value) {
            this.publish(event, value, true);
        }
    }

    /**
     * A publisher for VNAV-related data events.
     */
    class VNavDataEventPublisher extends BasePublisher {
        /**
         * Creates a new instance of VNavDataEventPublisher.
         * @param bus The event bus to which to publish.
         */
        constructor(bus) {
            super(bus);
        }
        /**
         * Publish a control event.
         * @param event The event from ControlEvents.
         * @param value The value of the event.
         */
        publishEvent(event, value) {
            this.publish(event, value, true);
        }
    }

    exports.APStates = void 0;
    (function (APStates) {
        APStates[APStates["None"] = 0] = "None";
        APStates[APStates["APActive"] = 1] = "APActive";
        APStates[APStates["YawDamper"] = 2] = "YawDamper";
        APStates[APStates["Heading"] = 4] = "Heading";
        APStates[APStates["Nav"] = 8] = "Nav";
        APStates[APStates["NavArmed"] = 16] = "NavArmed";
        APStates[APStates["Approach"] = 32] = "Approach";
        APStates[APStates["ApproachArmed"] = 64] = "ApproachArmed";
        APStates[APStates["Backcourse"] = 128] = "Backcourse";
        APStates[APStates["BackcourseArmed"] = 256] = "BackcourseArmed";
        APStates[APStates["Alt"] = 512] = "Alt";
        APStates[APStates["AltS"] = 1024] = "AltS";
        APStates[APStates["AltV"] = 2048] = "AltV";
        APStates[APStates["VS"] = 4096] = "VS";
        APStates[APStates["FLC"] = 8192] = "FLC";
        APStates[APStates["GP"] = 16384] = "GP";
        APStates[APStates["GPArmed"] = 32768] = "GPArmed";
        APStates[APStates["GS"] = 65536] = "GS";
        APStates[APStates["GSArmed"] = 131072] = "GSArmed";
        APStates[APStates["Path"] = 262144] = "Path";
        APStates[APStates["PathArmed"] = 524288] = "PathArmed";
        APStates[APStates["PathInvalid"] = 1048576] = "PathInvalid";
        APStates[APStates["Pitch"] = 2097152] = "Pitch";
        APStates[APStates["Roll"] = 4194304] = "Roll";
        APStates[APStates["VNAV"] = 8388608] = "VNAV";
        APStates[APStates["ATSpeed"] = 16777216] = "ATSpeed";
        APStates[APStates["ATMach"] = 33554432] = "ATMach";
        APStates[APStates["ATArmed"] = 67108864] = "ATArmed";
        APStates[APStates["FD"] = 134217728] = "FD";
    })(exports.APStates || (exports.APStates = {}));

    exports.AutopilotModeVars = void 0;
    (function (AutopilotModeVars) {
        AutopilotModeVars["LateralArmed"] = "L:WTAP_Lateral_Armed_Mode";
        AutopilotModeVars["LateralActive"] = "L:WTAP_Lateral_Active_Mode";
        AutopilotModeVars["VerticalArmed"] = "L:WTAP_Vertical_Armed_Mode";
        AutopilotModeVars["VerticalActive"] = "L:WTAP_Vertical_Active_Mode";
    })(exports.AutopilotModeVars || (exports.AutopilotModeVars = {}));

    /**
     * An autopilot driver to set pitch and bank values in the sim AP.
     *
     * This driver follows the sim's convention for negative and positive values:
     * AUTOPILOT BANK HOLD REF/PLANE BANK DEGREES: negative = right, positive = left
     * AUTOPILOT PITCH HOLD REF/PLANE PITCH DEGREES: negative = up, positive = down
     * INCIDENCE ALPHA: negative = down, positive = up
     * AMBIENT WIND Y: negative = down, positive = up
     */
    class AutopilotDriver {
        /**
         * Creates an instance of this Autopilot Driver.
         * @param bus An instance of the Event Bus.
         * @param apValues Autopilot values from this driver's parent autopilot.
         * @param apMasterOn Whether the AP is engaged.
         * @param options Options for this driver.
         */
        constructor(bus, apValues, apMasterOn, options) {
            var _a, _b, _c;
            this.apValues = apValues;
            this.apMasterOn = apMasterOn;
            this.currentPitchRef = 0;
            this.currentBankRef = 0;
            this.rudderBankFactor = 0.3;
            this.maxRudderDeflection = 25;
            this.autoCoordinationEnabled = false;
            this.rudderSet = 0;
            this.verticalWindSmoother = new ExpSmoother(AutopilotDriver.VERTICAL_WIND_SMOOTHING_TAU);
            this.verticalWindAverageValue = 0;
            this.onGround = ConsumerSubject.create(null, true);
            this.raHeight = ConsumerValue.create(null, null);
            if (options === null || options === void 0 ? void 0 : options.setInternalFlightDirector) {
                this.fdPublisher = bus.getPublisher();
            }
            this.pitchServoRate = (_a = options === null || options === void 0 ? void 0 : options.pitchServoRate) !== null && _a !== void 0 ? _a : AutopilotDriver.PITCH_SERVO_RATE;
            this.pitchServo = new LinearServo(this.pitchServoRate);
            this.currentPitchRef = SimVar.GetSimVarValue('AUTOPILOT PITCH HOLD REF', exports.SimVarValueType.Degree);
            this.bankServoRate = (_b = options === null || options === void 0 ? void 0 : options.bankServoRate) !== null && _b !== void 0 ? _b : AutopilotDriver.BANK_SERVO_RATE;
            this.bankServo = new LinearServo(this.bankServoRate);
            this.currentBankRef = SimVar.GetSimVarValue('AUTOPILOT BANK HOLD REF', exports.SimVarValueType.Degree);
            if (options === null || options === void 0 ? void 0 : options.autoCoordinationEnabled) {
                if (options.rudderBankFactor !== undefined) {
                    this.rudderBankFactor = options.rudderBankFactor;
                }
                if (options.maxRudderDeflection !== undefined) {
                    this.maxRudderDeflection = options.maxRudderDeflection;
                }
                this.autoCoordinationEnabled = true;
                this.onGround.setConsumer(bus.getSubscriber().on('on_ground'));
                this.raHeight.setConsumer(bus.getSubscriber().on('radio_alt'));
                this.apMasterOn.sub(isOn => {
                    if (!isOn) {
                        this.resetRudder();
                    }
                });
                this.onGround.sub(onGround => {
                    if (onGround) {
                        this.resetRudder();
                    }
                });
            }
            if (options === null || options === void 0 ? void 0 : options.zeroRollHeight) {
                this.zeroRollHeight = options.zeroRollHeight;
            }
            this.rudderServoRate = ((_c = options === null || options === void 0 ? void 0 : options.rudderServoRate) !== null && _c !== void 0 ? _c : AutopilotDriver.RUDDER_SERVO_RATE) / this.maxRudderDeflection * 16384;
            this.rudderServo = new LinearServo(this.rudderServoRate);
            this.apValues.simRate.sub(this.onSimRateChanged.bind(this), true);
        }
        /**
         * Responds to when the simulation rate changes.
         * @param simRate The new simulation rate.
         */
        onSimRateChanged(simRate) {
            this.pitchServo.rate = this.pitchServoRate * simRate;
            this.bankServo.rate = this.bankServoRate * simRate;
            this.rudderServo.rate = this.rudderServoRate * simRate;
        }
        /**
         * Update loop to keep Ambient Wind Y constantly updated.
         */
        update() {
            const verticalWind = SimVar.GetSimVarValue('AMBIENT WIND Y', exports.SimVarValueType.FPM);
            const time = Date.now();
            if (this._lastVerticalWindTime === undefined) {
                this.verticalWindAverageValue = this.verticalWindSmoother.reset(verticalWind);
            }
            else {
                this.verticalWindAverageValue = this.verticalWindSmoother.next(verticalWind, time - this._lastVerticalWindTime);
            }
            this._lastVerticalWindTime = time;
            if (this.autoCoordinationEnabled && this.apMasterOn.get() === true && this.onGround.get() === false) {
                this.manageAutoRudder();
            }
        }
        /**
         * Drives the commanded autopilot bank angle toward a desired value using a linear servo.
         * @param bank The desired bank angle, in degrees. Positive values indicate left bank.
         * @param rate The rate at which to drive the commanded bank angle, in degrees per second. Defaults to the bank
         * servo's default rate.
         */
        driveBank(bank, rate) {
            if (isFinite(bank)) {
                const currentTime = Date.now();
                if (this._lastBankSetTime !== undefined) {
                    const deltaTime = currentTime - this._lastBankSetTime;
                    if (deltaTime > 1000) {
                        this.bankServo.reset();
                    }
                }
                else {
                    this.bankServo.reset();
                }
                this._lastBankSetTime = currentTime;
                this.setBank(this.bankServo.drive(this.currentBankRef, bank, currentTime, rate), false);
            }
            else {
                console.warn('AutopilotDriver: Non-finite bank angle was attempted to be set.');
            }
        }
        /**
         * Sets the commanded autopilot bank angle, in degrees.
         * @param bank The commanded bank angle, in degrees. Positive values indicate left bank.
         * @param resetServo Whether to reset the bank servo. Defaults to `true`.
         */
        setBank(bank, resetServo = true) {
            var _a;
            if (this.zeroRollHeight !== undefined) {
                const raHeight = this.raHeight.get();
                if (raHeight !== null && raHeight < this.zeroRollHeight) {
                    bank = 0;
                }
            }
            if (isFinite(bank)) {
                this.currentBankRef = bank;
                SimVar.SetSimVarValue('AUTOPILOT BANK HOLD REF', exports.SimVarValueType.Degree, this.currentBankRef);
                (_a = this.fdPublisher) === null || _a === void 0 ? void 0 : _a.pub('fd_target_bank', this.currentBankRef, true, true);
            }
            else {
                console.warn('AutopilotDriver: Non-finite bank angle was attempted to be set.');
            }
            if (resetServo) {
                this._lastBankSetTime = undefined;
            }
        }
        /**
         * Drives the commanded autopilot pitch angle toward a desired value using a linear servo while optionally correcting
         * for angle of attack and vertical wind.
         * @param pitch The desired pitch angle, in degrees. Positive values indicate downward pitch.
         * @param adjustForAoa Whether to adjust the commanded pitch angle for angle of attack. If `true`, the provided pitch
         * angle is treated as a desired flight path angle and a new commanded pitch angle will be calculated to produce the
         * desired FPA given the airplane's current angle of attack. This correction can be used in conjunction with the
         * vertical wind correction. Defaults to `false`.
         * @param adjustForVerticalWind Whether to adjust the commanded pitch angle for vertical wind velocity. If `true`,
         * the provided pitch angle is treated as a desired flight path angle and a new commanded pitch angle will be
         * calculated to produce the desired FPA given the current vertical wind component. This correction can be used in
         * conjunction with the angle of attack correction. Defaults to `false`.
         * @param rate The rate at which to drive the commanded pitch angle, in degrees per second. Defaults to the pitch
         * servo's default rate.
         * @param maxNoseDownPitch The maximum nose down pitch angle, defaults to the global AP pitch limit.
         * @param maxNoseUpPitch The maximum nose up pitch angle, defaults to the global AP pitch limit.
         */
        drivePitch(pitch, adjustForAoa = false, adjustForVerticalWind = false, rate, maxNoseDownPitch = this.apValues.maxNoseDownPitchAngle.get(), maxNoseUpPitch = this.apValues.maxNoseUpPitchAngle.get()) {
            pitch = this.getAdjustedPitch(pitch, adjustForAoa, adjustForVerticalWind);
            if (isFinite(pitch)) {
                const currentTime = Date.now();
                if (this._lastPitchSetTime !== undefined) {
                    const deltaTime = currentTime - this._lastPitchSetTime;
                    if (deltaTime > 1000) {
                        this.pitchServo.reset();
                    }
                }
                else {
                    this.pitchServo.reset();
                }
                this._lastPitchSetTime = currentTime;
                const limitedPitch = MathUtils.clamp(pitch, -maxNoseDownPitch, maxNoseUpPitch);
                this.setPitch(this.pitchServo.drive(this.currentPitchRef, limitedPitch, currentTime, rate), false, maxNoseDownPitch, maxNoseUpPitch);
            }
            else {
                console.warn('AutopilotDriver: Non-finite pitch angle was attempted to be set.');
            }
        }
        /**
         * Sets the commanded autopilot pitch angle, in degrees.
         * @param pitch The commanded pitch angle, in degrees. Positive values indicate downward pitch.
         * @param resetServo Whether to reset the pitch servo. Defaults to `true`.
         * @param maxNoseDownPitch The maximum nose down pitch angle, defaults to the global AP pitch limit.
         * @param maxNoseUpPitch The maximum nose up pitch angle, defaults to the global AP pitch limit.
         */
        setPitch(pitch, resetServo = true, maxNoseDownPitch = this.apValues.maxNoseDownPitchAngle.get(), maxNoseUpPitch = this.apValues.maxNoseUpPitchAngle.get()) {
            var _a;
            if (isFinite(pitch)) {
                this.currentPitchRef = MathUtils.clamp(pitch, -maxNoseDownPitch, maxNoseUpPitch);
                SimVar.SetSimVarValue('AUTOPILOT PITCH HOLD REF', exports.SimVarValueType.Degree, this.currentPitchRef);
                (_a = this.fdPublisher) === null || _a === void 0 ? void 0 : _a.pub('fd_target_pitch', this.currentPitchRef, true, true);
            }
            else {
                console.warn('AutopilotDriver: Non-finite pitch angle was attempted to be set.');
            }
            if (resetServo) {
                this._lastPitchSetTime = undefined;
            }
        }
        /**
         * Adjusts a pitch angle optionally for AOA and vertical wind.
         * @param pitch The desired pitch angle, in degrees. Positive values indicate downward pitch.
         * @param adjustForAoa Whether to adjust the commanded pitch angle for angle of attack. If `true`, the provided pitch
         * angle is treated as a desired flight path angle and a new commanded pitch angle will be calculated to produce the
         * desired FPA given the airplane's current angle of attack. This correction can be used in conjunction with the
         * vertical wind correction. Defaults to `false`.
         * @param adjustForVerticalWind Whether to adjust the commanded pitch angle for vertical wind velocity. If `true`,
         * the provided pitch angle is treated as a desired flight path angle and a new commanded pitch angle will be
         * calculated to produce the desired FPA given the current vertical wind component. This correction can be used in
         * conjunction with the angle of attack correction. Defaults to `false`.
         * @returns The adjusted pitch.
         */
        getAdjustedPitch(pitch, adjustForAoa = false, adjustForVerticalWind = false) {
            if (!isFinite(pitch)) {
                return pitch;
            }
            //pitch = -5 we want a 5 degree FPA up
            if (adjustForVerticalWind) {
                // with an updraft, we get a down correction value
                // if pitch were normal (+ === up), we would add the correction 5 + (-1) = 4 (pitch adjusted down because of updraft)
                // since pitch is actually inverse (- === up), we want to subtract the correction value -5 - (-1) = -4
                pitch -= this.getVerticalWindCorrection();
            }
            if (adjustForAoa) {
                // if we want to fly an FPA of +5 degrees, we need to add our AOA to our FPA for the desired pitch.
                // if our AOA is 1 degree, we want to set our pitch to 5 + 1 = 6 degrees to achieve a 5 degree FPA.
                // since pitch is inverse and AOA is not, we want to subtract the aoa value -5 - (+1) = -6 (6 degree up pitch)
                // if we are wanting to fly an FPA of -3 degrees, and our AOA is +1 degree, we would set +3 - (+1) = 2 (2 degree down pitch)
                pitch -= SimVar.GetSimVarValue('INCIDENCE ALPHA', exports.SimVarValueType.Degree);
            }
            return pitch;
        }
        /**
         * Gets the vertical wind correction in degrees.
         * @returns The vertical wind correction in degrees.
         */
        getVerticalWindCorrection() {
            // Wind correction FPA will be the FPA required to negate the vertical wind (so negative verticalWindAverageValue)
            return VNavUtils.getFpa(SimVar.GetSimVarValue('AIRSPEED TRUE', exports.SimVarValueType.FPM), -this.verticalWindAverageValue);
        }
        /**
         * Manages the Auto Rudder in Autopilot.
         */
        manageAutoRudder() {
            this.rudderSet = this.rudderServo.drive(this.rudderSet, (this.rudderBankFactor * this.currentBankRef / this.maxRudderDeflection) * 16384);
            SimVar.SetSimVarValue('K:AXIS_RUDDER_SET', exports.SimVarValueType.Number, this.rudderSet);
        }
        /**
         * Resets the rudder to 0.
         */
        resetRudder() {
            SimVar.SetSimVarValue('K:AXIS_RUDDER_SET', exports.SimVarValueType.Number, 0);
            this.rudderServo.reset();
        }
    }
    AutopilotDriver.PITCH_SERVO_RATE = 5; // degrees per second
    AutopilotDriver.BANK_SERVO_RATE = 10; // degrees per second
    AutopilotDriver.RUDDER_SERVO_RATE = 1; // degrees per second
    AutopilotDriver.VERTICAL_WIND_SMOOTHING_TAU = 500 / Math.LN2;

    /// <reference types="@microsoft/msfs-types/coherent/apcontroller" />
    /**
     * An Autopilot.
     */
    class Autopilot {
        /**
         * Creates an instance of the Autopilot.
         * @param bus The event bus.
         * @param flightPlanner This autopilot's associated flight planner.
         * @param config This autopilot's configuration.
         * @param stateManager This autopilot's state manager.
         */
        constructor(bus, flightPlanner, config, stateManager) {
            var _a, _b, _c, _d;
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.config = config;
            this.stateManager = stateManager;
            this.cdiSource = { type: exports.NavSourceType.Nav, index: 0 };
            this.lateralModes = new Map();
            this.verticalModes = new Map();
            this.verticalAltitudeArmed = exports.APAltitudeModes.NONE;
            this.verticalApproachArmed = exports.APVerticalModes.NONE;
            this.altCapArmed = false;
            this.lateralModeFailed = false;
            this.inClimb = false;
            this.currentAltitude = 0;
            this.vnavCaptureType = exports.VNavAltCaptureType.None;
            this.flightPlanSynced = false;
            /** Can be set to false in child classes to override behavior for certain aircraft. */
            this.requireApproachIsActiveForNavToNav = true;
            this._apValues = {
                cdiId: (_a = this.config.cdiId) !== null && _a !== void 0 ? _a : '',
                cdiSource: Subject.create(this.cdiSource, CdiUtils.navSourceIdEquals),
                simRate: Subject.create(0),
                selectedAltitude: Subject.create(0),
                selectedVerticalSpeed: Subject.create(0),
                selectedFlightPathAngle: Subject.create(0),
                selectedIas: Subject.create(0),
                selectedMach: Subject.create(0),
                isSelectedSpeedInMach: Subject.create(false),
                selectedPitch: Subject.create(0),
                maxBankId: Subject.create(0),
                maxBankAngle: Subject.create(30),
                maxNoseUpPitchAngle: Subject.create(Infinity),
                maxNoseDownPitchAngle: Subject.create(Infinity),
                selectedHeading: Subject.create(0),
                capturedAltitude: Subject.create(0),
                approachIsActive: Subject.create(false),
                approachHasGP: Subject.create(false),
                nav1HasGs: Subject.create(false),
                nav2HasGs: Subject.create(false),
                nav3HasGs: Subject.create(false),
                nav4HasGs: Subject.create(false),
                lateralActive: Subject.create(exports.APLateralModes.NONE),
                verticalActive: Subject.create(exports.APVerticalModes.NONE),
                lateralArmed: Subject.create(exports.APLateralModes.NONE),
                verticalArmed: Subject.create(exports.APVerticalModes.NONE),
                apApproachModeOn: Subject.create(false)
            };
            this.apValues = this._apValues;
            this.autopilotInitialized = false;
            if (config.defaultMaxBankAngle !== undefined) {
                this.apValues.maxBankAngle.set(config.defaultMaxBankAngle);
            }
            if (config.defaultMaxNoseUpPitchAngle !== undefined) {
                this.apValues.maxNoseUpPitchAngle.set(config.defaultMaxNoseUpPitchAngle);
            }
            if (config.defaultMaxNoseDownPitchAngle !== undefined) {
                this.apValues.maxNoseDownPitchAngle.set(config.defaultMaxNoseDownPitchAngle);
            }
            this.createDirectors(config);
            this.vnavManager = (_b = config.createVNavManager) === null || _b === void 0 ? void 0 : _b.call(config, this.apValues);
            this.navToNavManager = (_c = config.createNavToNavManager) === null || _c === void 0 ? void 0 : _c.call(config, this.apValues);
            this.variableBankManager = (_d = config.createVariableBankManager) === null || _d === void 0 ? void 0 : _d.call(config, this.apValues);
            if (this.navToNavManager) {
                this.apValues.navToNavArmableNavRadioIndex = this.navToNavManager.getArmableNavRadioIndex.bind(this.navToNavManager);
                this.apValues.navToNavArmableLateralMode = this.navToNavManager.getArmableLateralMode.bind(this.navToNavManager);
                this.apValues.navToNavArmableVerticalMode = this.navToNavManager.getArmableVerticalMode.bind(this.navToNavManager);
                this.apValues.navToNavTransferInProgress = this.navToNavManager.isTransferInProgress.bind(this.navToNavManager);
            }
            this.stateManager.stateManagerInitialized.sub((v) => {
                if (v) {
                    this.autopilotInitialized = true;
                }
                else {
                    this.autopilotInitialized = false;
                }
                this.onInitialized();
            });
            this.apDriver = new AutopilotDriver(this.bus, this.apValues, this.stateManager.apMasterOn, this.config.autopilotDriverOptions);
            this.initLateralModes();
            this.initVerticalModes();
            this.initNavToNavManager();
            this.initVNavManager();
            this.monitorEvents();
        }
        /**
         * Creates this autopilot's directors.
         * @param config This autopilot's configuration.
         */
        createDirectors(config) {
            this.createLateralDirectors(config);
            this.createVerticalDirectors(config);
        }
        /**
         * Creates this autopilot's lateral mode directors.
         * @param config This autopilot's configuration.
         */
        createLateralDirectors(config) {
            if (config.createLateralDirectors) {
                for (const { mode, director } of config.createLateralDirectors(this.apValues)) {
                    if (mode !== exports.APLateralModes.NONE) {
                        this.lateralModes.set(mode, director);
                    }
                }
            }
            this.lateralModes.set(exports.APLateralModes.NONE, new APNoneLateralDirector());
        }
        /**
         * Creates this autopilot's vertical mode directors.
         * @param config This autopilot's configuration.
         */
        createVerticalDirectors(config) {
            if (config.createVerticalDirectors) {
                for (const { mode, director } of config.createVerticalDirectors(this.apValues)) {
                    if (mode !== exports.APVerticalModes.NONE) {
                        this.verticalModes.set(mode, director);
                    }
                }
            }
            this.verticalModes.set(exports.APVerticalModes.NONE, new APNoneVerticalDirector());
        }
        /**
         * Initializes this autopilot's lateral modes.
         */
        initLateralModes() {
            const setBank = this.apDriver.setBank.bind(this.apDriver);
            const driveBank = this.apDriver.driveBank.bind(this.apDriver);
            for (const [mode, director] of this.lateralModes) {
                this.initLateralModeDirector(mode, director, setBank, driveBank);
            }
        }
        /**
         * Initializes a lateral mode director.
         * @param mode The director's mode.
         * @param director The director to initialize.
         * @param setBank A function that the director can use to set the flight director's commanded bank angle.
         * @param driveBank A function that the director can use to drive the flight director's commanded bank angle.
         */
        initLateralModeDirector(mode, director, setBank, driveBank) {
            director.onArm = () => { this.setLateralArmed(mode); };
            director.onActivate = () => { this.setLateralActive(mode); };
            director.setBank = setBank;
            director.driveBank = driveBank;
        }
        /**
         * Initializes this autopilot's vertical modes.
         */
        initVerticalModes() {
            const setPitch = this.apDriver.setPitch.bind(this.apDriver);
            const drivePitch = this.apDriver.drivePitch.bind(this.apDriver);
            for (const [mode, director] of this.verticalModes) {
                this.initVerticalModeDirector(mode, director, setPitch, drivePitch);
            }
        }
        /**
         * Initializes a vertical mode director.
         * @param mode The director's mode.
         * @param director The director to initialize.
         * @param setPitch A function that the director can use to set the flight director's commanded pitch angle.
         * @param drivePitch A function that the director can use to drive the flight director's commanded pitch angle.
         */
        initVerticalModeDirector(mode, director, setPitch, drivePitch) {
            switch (mode) {
                case exports.APVerticalModes.ALT:
                    director.onArm = () => { this.setVerticalArmed(mode); };
                    director.onActivate = () => {
                        this.altCapArmed = false;
                        this.setVerticalActive(mode);
                    };
                    break;
                case exports.APVerticalModes.CAP:
                    director.onArm = () => {
                        var _a;
                        this.altCapArmed = true;
                        if (this.apValues.verticalArmed.get() === exports.APVerticalModes.ALT) {
                            (_a = this.verticalModes.get(exports.APVerticalModes.ALT)) === null || _a === void 0 ? void 0 : _a.deactivate();
                        }
                    };
                    director.onActivate = () => {
                        var _a;
                        this.altCapArmed = false;
                        this.setVerticalActive(mode);
                        (_a = this.verticalModes.get(exports.APVerticalModes.ALT)) === null || _a === void 0 ? void 0 : _a.arm();
                    };
                    break;
                case exports.APVerticalModes.PATH:
                    director.onArm = () => { this.setVerticalArmed(mode); };
                    director.onActivate = () => { this.setVerticalActive(mode); };
                    director.onDeactivate = () => { var _a; (_a = this.vnavManager) === null || _a === void 0 ? void 0 : _a.onPathDirectorDeactivated(); };
                    break;
                case exports.APVerticalModes.GP:
                    director.onArm = () => { this.setVerticalApproachArmed(mode); };
                    director.onActivate = () => {
                        var _a;
                        (_a = this.vnavManager) === null || _a === void 0 ? void 0 : _a.tryDeactivate(exports.APVerticalModes.NONE);
                        this.setVerticalActive(mode);
                        this.setVerticalArmed(exports.APVerticalModes.NONE);
                        this.setVerticalApproachArmed(exports.APVerticalModes.NONE);
                    };
                    break;
                case exports.APVerticalModes.GS:
                    director.onArm = () => { this.setVerticalApproachArmed(mode); };
                    director.onActivate = () => {
                        var _a;
                        this.setVerticalActive(mode);
                        (_a = this.verticalModes.get(exports.APVerticalModes.PATH)) === null || _a === void 0 ? void 0 : _a.deactivate();
                        this.setVerticalArmed(exports.APVerticalModes.NONE);
                        this.setVerticalApproachArmed(exports.APVerticalModes.NONE);
                    };
                    break;
                case exports.APVerticalModes.FLARE:
                    director.onArm = () => { this.setVerticalArmed(mode); };
                    director.onActivate = () => {
                        this.setVerticalActive(mode);
                        this.setVerticalArmed(exports.APVerticalModes.NONE);
                        this.setVerticalApproachArmed(exports.APVerticalModes.NONE);
                    };
                    break;
                case exports.APVerticalModes.LEVEL:
                    director.onArm = () => { this.setVerticalArmed(mode); };
                    director.onActivate = () => {
                        this.setVerticalActive(mode);
                        this.setVerticalArmed(exports.APVerticalModes.NONE);
                        this.setVerticalApproachArmed(exports.APVerticalModes.NONE);
                    };
                    break;
                default:
                    director.onArm = () => { this.setVerticalArmed(mode); };
                    director.onActivate = () => { this.setVerticalActive(mode); };
            }
            director.setPitch = setPitch;
            director.drivePitch = drivePitch;
        }
        /**
         * Initializes the Autopilot with the available Nav To Nav Manager.
         */
        initNavToNavManager() {
            if (this.navToNavManager) {
                this.navToNavManager.onTransferred = (activateLateralMode, activateVerticalMode) => {
                    var _a, _b;
                    if (activateLateralMode !== exports.APLateralModes.NONE && this.apValues.lateralActive.get() !== activateLateralMode) {
                        (_a = this.lateralModes.get(activateLateralMode)) === null || _a === void 0 ? void 0 : _a.activate();
                    }
                    if (activateVerticalMode !== exports.APVerticalModes.NONE && this.apValues.verticalActive.get() !== activateVerticalMode) {
                        (_b = this.verticalModes.get(activateVerticalMode)) === null || _b === void 0 ? void 0 : _b.activate();
                    }
                };
            }
        }
        /**
         * Initializes the Autopilot with the available VNav Manager.
         */
        initVNavManager() {
            if (this.vnavManager) {
                this.vnavManager.armMode = (mode) => {
                    var _a;
                    const armedMode = this.apValues.verticalArmed.get();
                    if (mode === exports.APVerticalModes.NONE && (armedMode === exports.APVerticalModes.PATH || armedMode === exports.APVerticalModes.FLC)) {
                        this.setVerticalArmed(mode);
                    }
                    else {
                        (_a = this.verticalModes.get(mode)) === null || _a === void 0 ? void 0 : _a.arm();
                    }
                };
                this.vnavManager.activateMode = (mode) => {
                    var _a, _b;
                    if (mode === exports.APVerticalModes.NONE && this.apValues.verticalActive.get() === exports.APVerticalModes.PATH) {
                        (_a = this.verticalModes.get(this.getDefaultVerticalMode())) === null || _a === void 0 ? void 0 : _a.activate();
                    }
                    else {
                        (_b = this.verticalModes.get(mode)) === null || _b === void 0 ? void 0 : _b.activate();
                    }
                };
            }
        }
        /**
         * Update method for the Autopilot.
         */
        update() {
            if (this.autopilotInitialized) {
                this.onBeforeUpdate();
                this.stateManager.onBeforeUpdate();
                this.updateNavToNavManagerBefore();
                this.apDriver.update();
                this.checkModes();
                this.manageAltitudeCapture();
                this.updateModes();
                this.updateNavToNavManagerAfter();
                this.stateManager.onAfterUpdate();
                this.onAfterUpdate();
            }
        }
        /**
         * This method runs each update cycle before the update occurs.
         */
        onBeforeUpdate() {
            // noop
        }
        /**
         * This method runs each update cycle after the update occurs.
         */
        onAfterUpdate() {
            // noop
        }
        /**
         * This method runs whenever the initialized state of the Autopilot changes.
         */
        onInitialized() {
            // noop
        }
        /**
         * Handles input from the State Manager when a lateral mode button is pressed.
         * @param data is the AP Lateral Mode Event Data
         */
        lateralPressed(data) {
            var _a, _b, _c, _d;
            const mode = data.mode;
            if (mode !== exports.APLateralModes.NAV && !this.lateralModes.has(mode)) {
                return;
            }
            const set = data.set;
            if (set === undefined || set === false) {
                if (this.deactivateArmedOrActiveLateralMode(mode)) {
                    return;
                }
            }
            if (set === undefined || set === true) {
                if (this.config.autoEngageFd !== false && !this.stateManager.isAnyFlightDirectorOn.get()) {
                    this.stateManager.setFlightDirector(true);
                }
                else if (this.config.autoEngageFd === false && !this.stateManager.isAnyFlightDirectorOn.get() && !this.stateManager.apMasterOn.get()) {
                    return;
                }
                switch (mode) {
                    case exports.APLateralModes.NONE:
                        break;
                    case exports.APLateralModes.LEVEL:
                    case exports.APLateralModes.ROLL:
                    case exports.APLateralModes.HEADING:
                    case exports.APLateralModes.HEADING_HOLD:
                    case exports.APLateralModes.TRACK:
                    case exports.APLateralModes.TRACK_HOLD:
                    case exports.APLateralModes.LOC:
                    case exports.APLateralModes.VOR:
                    case exports.APLateralModes.BC:
                    case exports.APLateralModes.FMS_LOC:
                        (_a = this.lateralModes.get(mode)) === null || _a === void 0 ? void 0 : _a.arm();
                        break;
                    case exports.APLateralModes.NAV:
                        if (this.cdiSource.type === exports.NavSourceType.Gps) {
                            (_b = this.lateralModes.get(exports.APLateralModes.GPSS)) === null || _b === void 0 ? void 0 : _b.arm();
                        }
                        else {
                            (_c = this.lateralModes.get(exports.APLateralModes.VOR)) === null || _c === void 0 ? void 0 : _c.arm();
                            (_d = this.lateralModes.get(exports.APLateralModes.LOC)) === null || _d === void 0 ? void 0 : _d.arm();
                        }
                        break;
                }
            }
        }
        /**
         * Handles input from the State Manager when a vertical mode button is pressed.
         * @param data is the AP Vertical Mode Event Data
         */
        verticalPressed(data) {
            var _a, _b, _c, _d, _e;
            const mode = data.mode;
            if (!this.verticalModes.has(mode)) {
                return;
            }
            const set = data.set;
            if ((set === undefined || set === false) && mode !== exports.APVerticalModes.TO) {
                if (this.deactivateArmedOrActiveVerticalMode(mode)) {
                    return;
                }
            }
            if (set === undefined || set === true) {
                if (this.config.autoEngageFd !== false && !this.stateManager.isAnyFlightDirectorOn.get()) {
                    this.stateManager.setFlightDirector(true);
                }
                else if (this.config.autoEngageFd === false && !this.stateManager.isAnyFlightDirectorOn.get() && !this.stateManager.apMasterOn.get()) {
                    return;
                }
                switch (mode) {
                    case exports.APVerticalModes.NONE:
                    case exports.APVerticalModes.PATH:
                        break;
                    case exports.APVerticalModes.ALT:
                        if (((_a = this.vnavManager) === null || _a === void 0 ? void 0 : _a.state) !== exports.VNavState.Enabled_Active ||
                            (this.vnavManager && this.vnavManager.state === exports.VNavState.Enabled_Active && this.vnavManager.canVerticalModeActivate(mode))) {
                            this.setAltHold();
                        }
                        break;
                    case exports.APVerticalModes.PITCH:
                    case exports.APVerticalModes.VS:
                    case exports.APVerticalModes.FPA:
                    case exports.APVerticalModes.FLC:
                    case exports.APVerticalModes.LEVEL:
                        if (((_b = this.vnavManager) === null || _b === void 0 ? void 0 : _b.state) === exports.VNavState.Enabled_Active && !this.vnavManager.canVerticalModeActivate(mode)) {
                            // If the VNav Manager is active, don't activate the mode until VNav Approves.
                            (_c = this.verticalModes.get(mode)) === null || _c === void 0 ? void 0 : _c.arm();
                        }
                        else {
                            (_d = this.verticalModes.get(mode)) === null || _d === void 0 ? void 0 : _d.activate();
                        }
                        break;
                    case exports.APVerticalModes.GP:
                    case exports.APVerticalModes.GS:
                        (_e = this.verticalModes.get(mode)) === null || _e === void 0 ? void 0 : _e.arm();
                        break;
                    case exports.APVerticalModes.TO:
                    case exports.APVerticalModes.GA:
                        this.togaPressed();
                }
            }
        }
        /**
         * Checks if a lateral mode is armed or active and if so, deactivates it.
         * @param mode The lateral mode to check and deactivate.
         * @returns Whether the specified mode was armed or active and deactivated by this method.
         * @deprecated Please use `deactivateArmedOrActiveLateralMode()` instead.
         */
        isLateralModeActivatedOrArmed(mode) {
            return this.deactivateArmedOrActiveLateralMode(mode);
        }
        /**
         * Attempts to deactivate an armed or active lateral mode.
         * @param mode The lateral mode to deactivate.
         * @returns Whether the specified mode was armed or active and deactivated by this method.
         */
        deactivateArmedOrActiveLateralMode(mode) {
            var _a, _b, _c, _d, _e, _f;
            const { lateralActive, lateralArmed } = this.apValues;
            switch (mode) {
                case lateralActive.get():
                    (_a = this.lateralModes.get(mode)) === null || _a === void 0 ? void 0 : _a.deactivate();
                    (_b = this.lateralModes.get(this.getDefaultLateralMode())) === null || _b === void 0 ? void 0 : _b.arm();
                    return true;
                case lateralArmed.get():
                    (_c = this.lateralModes.get(mode)) === null || _c === void 0 ? void 0 : _c.deactivate();
                    lateralArmed.set(exports.APLateralModes.NONE);
                    return true;
                case exports.APLateralModes.NAV: {
                    const activeNavMode = lateralActive.get() === exports.APLateralModes.LOC ? exports.APLateralModes.LOC
                        : lateralActive.get() === exports.APLateralModes.VOR ? exports.APLateralModes.VOR
                            : lateralActive.get() === exports.APLateralModes.GPSS ? exports.APLateralModes.GPSS
                                : lateralActive.get() === exports.APLateralModes.FMS_LOC ? exports.APLateralModes.FMS_LOC
                                    : exports.APLateralModes.NONE;
                    if (activeNavMode !== exports.APLateralModes.NONE) {
                        if (this.config.onlyDisarmLnavOnOffEvent === undefined || !this.config.onlyDisarmLnavOnOffEvent) {
                            (_d = this.lateralModes.get(activeNavMode)) === null || _d === void 0 ? void 0 : _d.deactivate();
                            (_e = this.lateralModes.get(this.getDefaultLateralMode())) === null || _e === void 0 ? void 0 : _e.arm();
                            lateralActive.set(this.getDefaultLateralMode());
                        }
                    }
                    const armedNavMode = lateralArmed.get() === exports.APLateralModes.LOC ? exports.APLateralModes.LOC
                        : lateralArmed.get() === exports.APLateralModes.VOR ? exports.APLateralModes.VOR
                            : lateralArmed.get() === exports.APLateralModes.GPSS ? exports.APLateralModes.GPSS
                                : lateralArmed.get() === exports.APLateralModes.FMS_LOC ? exports.APLateralModes.FMS_LOC
                                    : exports.APLateralModes.NONE;
                    if (armedNavMode !== exports.APLateralModes.NONE) {
                        (_f = this.lateralModes.get(armedNavMode)) === null || _f === void 0 ? void 0 : _f.deactivate();
                        lateralArmed.set(exports.APLateralModes.NONE);
                    }
                    if (armedNavMode !== exports.APLateralModes.NONE || activeNavMode !== exports.APLateralModes.NONE) {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * Attempts to deactivate an armed or active vertical mode.
         * @param mode The vertical mode to deactivate.
         * @returns Whether the specified mode was armed or active and deactivated by this method.
         */
        deactivateArmedOrActiveVerticalMode(mode) {
            var _a, _b, _c, _d;
            const { verticalActive, verticalArmed } = this.apValues;
            switch (mode) {
                case verticalActive.get():
                    (_a = this.verticalModes.get(mode)) === null || _a === void 0 ? void 0 : _a.deactivate();
                    verticalActive.set(this.getDefaultVerticalMode());
                    (_b = this.verticalModes.get(verticalActive.get())) === null || _b === void 0 ? void 0 : _b.arm();
                    return true;
                case verticalArmed.get():
                    if (mode !== exports.APVerticalModes.ALT) {
                        (_c = this.verticalModes.get(mode)) === null || _c === void 0 ? void 0 : _c.deactivate();
                        verticalArmed.set(exports.APVerticalModes.NONE);
                        return true;
                    }
                    break;
                case this.verticalApproachArmed:
                    (_d = this.verticalModes.get(mode)) === null || _d === void 0 ? void 0 : _d.deactivate();
                    this.verticalApproachArmed = exports.APVerticalModes.NONE;
                    return true;
            }
            return false;
        }
        /**
         * Handles input from the State Manager when the APPR button is pressed.
         * @param set is whether this event commands a specific set
         */
        approachPressed(set) {
            var _a, _b, _c, _d, _e, _f, _g, _h;
            if ((set === undefined || set === false) && this.deactivateArmedOrActiveVerticalMode(exports.APVerticalModes.GP)) {
                (_a = this.lateralModes.get(exports.APLateralModes.GPSS)) === null || _a === void 0 ? void 0 : _a.deactivate();
                return;
            }
            if ((set === undefined || set === false) && this.deactivateArmedOrActiveVerticalMode(exports.APVerticalModes.GS)) {
                (_b = this.lateralModes.get(exports.APLateralModes.LOC)) === null || _b === void 0 ? void 0 : _b.deactivate();
                return;
            }
            if (set === undefined || set === true) {
                switch (this.getArmableApproachType()) {
                    case exports.APLateralModes.LOC:
                        if (((_c = this.lateralModes.get(exports.APLateralModes.LOC)) === null || _c === void 0 ? void 0 : _c.state) === exports.DirectorState.Inactive) {
                            (_d = this.lateralModes.get(exports.APLateralModes.LOC)) === null || _d === void 0 ? void 0 : _d.arm();
                        }
                        (_e = this.verticalModes.get(exports.APVerticalModes.GS)) === null || _e === void 0 ? void 0 : _e.arm();
                        break;
                    case exports.APLateralModes.GPSS:
                        if (((_f = this.lateralModes.get(exports.APLateralModes.GPSS)) === null || _f === void 0 ? void 0 : _f.state) === exports.DirectorState.Inactive) {
                            (_g = this.lateralModes.get(exports.APLateralModes.GPSS)) === null || _g === void 0 ? void 0 : _g.arm();
                        }
                        (_h = this.verticalModes.get(exports.APVerticalModes.GP)) === null || _h === void 0 ? void 0 : _h.arm();
                        break;
                }
            }
        }
        /**
         * Handles input from the State Manager when the TOGA button is pressed
         * (K event AUTO_THROTTLE_TO_GA)
         */
        togaPressed() {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
            const hasToMode = this.verticalModes.has(exports.APVerticalModes.TO) && this.lateralModes.has(exports.APLateralModes.TO);
            const hasGaMode = this.verticalModes.has(exports.APVerticalModes.GA) && this.lateralModes.has(exports.APLateralModes.GA);
            const verticalActive = this.apValues.verticalActive.get();
            const lateralActive = this.apValues.lateralActive.get();
            let toGaWasActive = false;
            if (hasToMode && hasGaMode) {
                if (verticalActive === exports.APVerticalModes.TO || verticalActive === exports.APVerticalModes.GA) {
                    (_a = this.verticalModes.get(verticalActive)) === null || _a === void 0 ? void 0 : _a.deactivate();
                    toGaWasActive = true;
                }
                if (lateralActive === exports.APLateralModes.GA || lateralActive === exports.APLateralModes.TO) {
                    (_b = this.lateralModes.get(lateralActive)) === null || _b === void 0 ? void 0 : _b.deactivate();
                    toGaWasActive = true;
                }
                if (!toGaWasActive) {
                    if (Simplane.getIsGrounded()) {
                        (_c = this.verticalModes.get(exports.APVerticalModes.TO)) === null || _c === void 0 ? void 0 : _c.arm();
                        (_d = this.lateralModes.get(exports.APLateralModes.TO)) === null || _d === void 0 ? void 0 : _d.arm();
                    }
                    else {
                        if (this.config.deactivateAutopilotOnGa !== false) {
                            SimVar.SetSimVarValue('K:AUTOPILOT_OFF', 'number', 0);
                        }
                        (_e = this.verticalModes.get(exports.APVerticalModes.GA)) === null || _e === void 0 ? void 0 : _e.arm();
                        (_f = this.lateralModes.get(exports.APLateralModes.GA)) === null || _f === void 0 ? void 0 : _f.arm();
                    }
                }
            }
            else if (hasToMode) {
                if (verticalActive === exports.APVerticalModes.TO) {
                    (_g = this.verticalModes.get(exports.APVerticalModes.TO)) === null || _g === void 0 ? void 0 : _g.deactivate();
                    toGaWasActive = true;
                }
                if (lateralActive === exports.APLateralModes.TO) {
                    (_h = this.lateralModes.get(exports.APLateralModes.TO)) === null || _h === void 0 ? void 0 : _h.deactivate();
                    toGaWasActive = true;
                }
                if (!toGaWasActive) {
                    (_j = this.verticalModes.get(exports.APVerticalModes.TO)) === null || _j === void 0 ? void 0 : _j.arm();
                    (_k = this.lateralModes.get(exports.APLateralModes.TO)) === null || _k === void 0 ? void 0 : _k.arm();
                }
            }
        }
        /**
         * Returns the AP Lateral Mode that can be armed.
         * @returns The AP Lateral Mode that can be armed.
         */
        getArmableApproachType() {
            switch (this.cdiSource.type) {
                case exports.NavSourceType.Nav:
                    if (this.cdiSource.index === 1 && this.apValues.nav1HasGs.get()) {
                        return exports.APLateralModes.LOC;
                    }
                    else if (this.cdiSource.index === 2 && this.apValues.nav2HasGs.get()) {
                        return exports.APLateralModes.LOC;
                    }
                    else if (this.cdiSource.index === 3 && this.apValues.nav3HasGs.get()) {
                        return exports.APLateralModes.LOC;
                    }
                    else if (this.cdiSource.index === 4 && this.apValues.nav4HasGs.get()) {
                        return exports.APLateralModes.LOC;
                    }
                    break;
                case exports.NavSourceType.Gps:
                    if (this.apValues.approachIsActive.get() && this.apValues.approachHasGP.get()) {
                        return exports.APLateralModes.GPSS;
                    }
                    else if (this.navToNavManager) {
                        const navToNavArmableMode = this.navToNavManager.getArmableLateralMode();
                        if (navToNavArmableMode !== exports.APLateralModes.NONE) {
                            return navToNavArmableMode;
                        }
                    }
            }
            return exports.APLateralModes.NONE;
        }
        /**
         * Callback to set the lateral active mode.
         * @param mode is the mode being set.
         */
        setLateralActive(mode) {
            const { lateralActive, lateralArmed } = this.apValues;
            this.checkRollModeActive();
            if (lateralArmed.get() === mode) {
                lateralArmed.set(exports.APLateralModes.NONE);
            }
            if (mode !== lateralActive.get()) {
                const currentMode = this.lateralModes.get(lateralActive.get());
                currentMode === null || currentMode === void 0 ? void 0 : currentMode.deactivate();
                lateralActive.set(mode);
            }
        }
        /**
         * Callback to set the lateral armed mode.
         * @param mode is the mode being set.
         */
        setLateralArmed(mode) {
            const { lateralArmed } = this.apValues;
            const currentMode = this.lateralModes.get(lateralArmed.get());
            currentMode === null || currentMode === void 0 ? void 0 : currentMode.deactivate();
            lateralArmed.set(mode);
        }
        /**
         * Callback to set the vertical active mode.
         * @param mode is the mode being set.
         */
        setVerticalActive(mode) {
            const { verticalActive, verticalArmed } = this.apValues;
            this.checkPitchModeActive();
            if (verticalArmed.get() === mode) {
                verticalArmed.set(exports.APVerticalModes.NONE);
            }
            else if (this.verticalApproachArmed === mode) {
                this.verticalApproachArmed = exports.APVerticalModes.NONE;
            }
            if (mode !== verticalActive.get()) {
                const currentMode = this.verticalModes.get(verticalActive.get());
                if ((currentMode === null || currentMode === void 0 ? void 0 : currentMode.state) !== exports.DirectorState.Inactive) {
                    currentMode === null || currentMode === void 0 ? void 0 : currentMode.deactivate();
                }
                verticalActive.set(mode);
            }
        }
        /**
         * Callback to set the vertical armed mode.
         * @param mode is the mode being set.
         */
        setVerticalArmed(mode) {
            const { verticalArmed } = this.apValues;
            if (mode !== verticalArmed.get()) {
                const currentMode = this.verticalModes.get(verticalArmed.get());
                if ((currentMode === null || currentMode === void 0 ? void 0 : currentMode.state) !== exports.DirectorState.Inactive) {
                    currentMode === null || currentMode === void 0 ? void 0 : currentMode.deactivate();
                }
            }
            verticalArmed.set(mode);
        }
        /**
         * Callback to set the vertical approach armed mode.
         * @param mode is the mode being set.
         */
        setVerticalApproachArmed(mode) {
            if (this.verticalApproachArmed === mode) {
                return;
            }
            const currentMode = this.verticalModes.get(this.verticalApproachArmed);
            currentMode === null || currentMode === void 0 ? void 0 : currentMode.deactivate();
            this.verticalApproachArmed = mode;
        }
        /**
         * Method called when the ALT button is pressed.
         */
        setAltHold() {
            var _a;
            if (this.verticalModes.has(exports.APVerticalModes.ALT)) {
                const currentAlt = 10 * (this.inClimb ? Math.ceil(this.currentAltitude / 10) : Math.floor(this.currentAltitude / 10));
                this.apValues.capturedAltitude.set(currentAlt);
                (_a = this.verticalModes.get(exports.APVerticalModes.ALT)) === null || _a === void 0 ? void 0 : _a.activate();
            }
        }
        /**
         * Checks if all the active and armed modes are still in their proper state
         * and takes corrective action if not.
         */
        checkModes() {
            var _a, _b, _c, _d, _e, _f, _g;
            if (this.lateralModeFailed) {
                this.lateralModeFailed = false;
            }
            if (!this.stateManager.apMasterOn.get() && !this.stateManager.isAnyFlightDirectorOn.get()) {
                return;
            }
            const { lateralActive, lateralArmed, verticalActive, verticalArmed } = this.apValues;
            if (!this.lateralModes.has(lateralActive.get()) || ((_a = this.lateralModes.get(lateralActive.get())) === null || _a === void 0 ? void 0 : _a.state) !== exports.DirectorState.Active) {
                if (lateralActive.get() !== exports.APLateralModes.NONE) {
                    this.lateralModeFailed = true;
                }
                (_b = this.lateralModes.get(this.getDefaultLateralMode())) === null || _b === void 0 ? void 0 : _b.arm();
            }
            if (lateralArmed.get() !== exports.APLateralModes.NONE
                && (!this.lateralModes.has(lateralArmed.get()) || ((_c = this.lateralModes.get(lateralArmed.get())) === null || _c === void 0 ? void 0 : _c.state) !== exports.DirectorState.Armed)) {
                this.setLateralArmed(exports.APLateralModes.NONE);
            }
            if (!this.verticalModes.has(verticalActive.get()) || ((_d = this.verticalModes.get(verticalActive.get())) === null || _d === void 0 ? void 0 : _d.state) !== exports.DirectorState.Active) {
                (_e = this.verticalModes.get(this.getDefaultVerticalMode())) === null || _e === void 0 ? void 0 : _e.arm();
            }
            if (verticalArmed.get() !== exports.APVerticalModes.NONE
                && (!this.verticalModes.has(verticalArmed.get()) || ((_f = this.verticalModes.get(verticalArmed.get())) === null || _f === void 0 ? void 0 : _f.state) !== exports.DirectorState.Armed)) {
                this.setVerticalArmed(exports.APVerticalModes.NONE);
            }
            if (this.verticalApproachArmed !== exports.APVerticalModes.NONE &&
                (!this.verticalModes.has(this.verticalApproachArmed) || ((_g = this.verticalModes.get(this.verticalApproachArmed)) === null || _g === void 0 ? void 0 : _g.state) !== exports.DirectorState.Armed)) {
                this.setVerticalApproachArmed(exports.APVerticalModes.NONE);
            }
        }
        /**
         * Runs update on each of the active and armed modes.
         */
        updateModes() {
            var _a, _b, _c, _d, _e, _f, _g;
            const { lateralActive, lateralArmed, verticalActive, verticalArmed } = this.apValues;
            if (lateralActive.get() !== exports.APLateralModes.NONE && this.lateralModes.has(lateralActive.get())) {
                (_a = this.lateralModes.get(lateralActive.get())) === null || _a === void 0 ? void 0 : _a.update();
            }
            if (lateralArmed.get() !== exports.APLateralModes.NONE && this.lateralModes.has(lateralArmed.get())) {
                (_b = this.lateralModes.get(lateralArmed.get())) === null || _b === void 0 ? void 0 : _b.update();
            }
            if (verticalActive.get() !== exports.APVerticalModes.NONE && this.verticalModes.has(verticalActive.get())) {
                (_c = this.verticalModes.get(verticalActive.get())) === null || _c === void 0 ? void 0 : _c.update();
            }
            if (verticalArmed.get() !== exports.APVerticalModes.NONE && this.verticalModes.has(verticalArmed.get())) {
                (_d = this.verticalModes.get(verticalArmed.get())) === null || _d === void 0 ? void 0 : _d.update();
            }
            if (this.verticalApproachArmed !== exports.APVerticalModes.NONE && this.verticalModes.has(this.verticalApproachArmed)) {
                (_e = this.verticalModes.get(this.verticalApproachArmed)) === null || _e === void 0 ? void 0 : _e.update();
            }
            if (this.altCapArmed) {
                (_f = this.verticalModes.get(exports.APVerticalModes.CAP)) === null || _f === void 0 ? void 0 : _f.update();
            }
            //while vnav and vnav director are one in the same we always want to
            //run the vnav update cycle no matter the director state
            (_g = this.vnavManager) === null || _g === void 0 ? void 0 : _g.update();
        }
        /**
         * Checks and sets the proper armed altitude mode.
         */
        manageAltitudeCapture() {
            var _a, _b, _c;
            let altCapType = exports.APAltitudeModes.NONE;
            let armAltCap = false;
            switch (this.apValues.verticalActive.get()) {
                case exports.APVerticalModes.VS:
                case exports.APVerticalModes.FPA:
                case exports.APVerticalModes.FLC:
                case exports.APVerticalModes.PITCH:
                case exports.APVerticalModes.TO:
                case exports.APVerticalModes.GA:
                    //PM Modif altitude capture
                    if (this.inClimb && this.apValues.selectedAltitude.get() > this.currentAltitude && this.altitudeArming) {
                        altCapType = exports.APAltitudeModes.ALTS;
                        armAltCap = true;
                    }
                    else if (!this.inClimb && this.apValues.selectedAltitude.get() < this.currentAltitude && this.altitudeArming) {
                        altCapType = exports.APAltitudeModes.ALTS;
                        armAltCap = true;
                    }
                    //End PM Modif
                    break;
                case exports.APVerticalModes.PATH: {
                    if (!this.inClimb) {
                        altCapType = this.vnavCaptureType === exports.VNavAltCaptureType.VNAV ? exports.APAltitudeModes.ALTV : exports.APAltitudeModes.ALTS;
                    }
                    break;
                }
                case exports.APVerticalModes.CAP:
                    altCapType = this.verticalAltitudeArmed;
                    break;
            }
            if (this.verticalAltitudeArmed !== altCapType) {
                this.verticalAltitudeArmed = altCapType;
            }
            //PM Modif
            // Block auto arming
            if (!this.altitudeArming)
                armAltCap = false;
            //End PM Modif
            if (armAltCap && (!this.altCapArmed || ((_a = this.verticalModes.get(exports.APVerticalModes.CAP)) === null || _a === void 0 ? void 0 : _a.state) === exports.DirectorState.Inactive)) {
                (_b = this.verticalModes.get(exports.APVerticalModes.CAP)) === null || _b === void 0 ? void 0 : _b.arm();
            }
            else if (!armAltCap && this.altCapArmed) {
                (_c = this.verticalModes.get(exports.APVerticalModes.CAP)) === null || _c === void 0 ? void 0 : _c.deactivate();
                this.altCapArmed = false;
            }
        }
        /**
         * Updates this autopilot's nav-to-nav manager before directors have been updated.
         */
        updateNavToNavManagerBefore() {
            var _a;
            (_a = this.navToNavManager) === null || _a === void 0 ? void 0 : _a.onBeforeUpdate();
        }
        /**
         * Updates this autopilot's nav-to-nav manager after directors have been updated.
         */
        updateNavToNavManagerAfter() {
            var _a;
            (_a = this.navToNavManager) === null || _a === void 0 ? void 0 : _a.onAfterUpdate();
        }
        /**
         * Monitors subevents and bus events.
         */
        monitorEvents() {
            var _a;
            this.stateManager.lateralPressed.on((sender, data) => {
                if (this.autopilotInitialized && data !== undefined) {
                    this.lateralPressed(data);
                }
            });
            this.stateManager.verticalPressed.on((sender, data) => {
                if (this.autopilotInitialized && data !== undefined) {
                    this.verticalPressed(data);
                }
            });
            this.stateManager.approachPressed.on((sender, data) => {
                if (this.autopilotInitialized) {
                    this.approachPressed(data);
                }
            });
            this.stateManager.vnavPressed.on((sender, data) => {
                var _a, _b;
                if (this.autopilotInitialized) {
                    if (data === true) {
                        (_a = this.vnavManager) === null || _a === void 0 ? void 0 : _a.tryActivate();
                    }
                    else {
                        (_b = this.vnavManager) === null || _b === void 0 ? void 0 : _b.tryDeactivate();
                    }
                }
            });
            // Sets up the subs for selected speed, selected mach and selected is mach.
            this.monitorApSpeedValues();
            const cdi = this.bus.getSubscriber();
            cdi.on(`cdi_select${CdiUtils.getEventBusTopicSuffix(this.apValues.cdiId)}`).handle((src) => {
                this.cdiSource = src;
                this._apValues.cdiSource.set(src);
            });
            const clock = this.bus.getSubscriber();
            clock.on('simRate').withPrecision(0).handle(this.apValues.simRate.set.bind(this.apValues.simRate));
            const ap = this.bus.getSubscriber();
            ap.on(`ap_altitude_selected_${(_a = this.config.altitudeHoldSlotIndex) !== null && _a !== void 0 ? _a : 1}`).withPrecision(0).handle((alt) => {
                this.apValues.selectedAltitude.set(alt);
            });
            ap.on('ap_heading_selected').withPrecision(0).handle((hdg) => {
                this.apValues.selectedHeading.set(hdg);
            });
            ap.on('ap_pitch_selected').withPrecision(1).handle((pitch) => {
                this.apValues.selectedPitch.set(pitch);
            });
            ap.on('ap_vs_selected').withPrecision(0).handle((ias) => {
                this.apValues.selectedVerticalSpeed.set(ias);
            });
            ap.on('ap_fpa_selected').withPrecision(1).handle((fpa) => {
                this.apValues.selectedFlightPathAngle.set(fpa);
            });
            ap.on('ap_max_bank_id').handle(id => {
                this.apValues.maxBankId.set(id);
            });
            const navproc = this.bus.getSubscriber();
            navproc.on('nav_glideslope_1').whenChanged().handle((hasgs) => {
                this.apValues.nav1HasGs.set(hasgs);
            });
            navproc.on('nav_glideslope_2').whenChanged().handle((hasgs) => {
                this.apValues.nav2HasGs.set(hasgs);
            });
            navproc.on('nav_glideslope_3').whenChanged().handle((hasgs) => {
                this.apValues.nav3HasGs.set(hasgs);
            });
            navproc.on('nav_glideslope_4').whenChanged().handle((hasgs) => {
                this.apValues.nav4HasGs.set(hasgs);
            });
            const adc = this.bus.getSubscriber();
            adc.on('vertical_speed').withPrecision(0).handle((vs) => {
                this.inClimb = vs < 1 ? false : true;
            });
            adc.on('indicated_alt').withPrecision(0).handle(alt => {
                this.currentAltitude = alt;
            });
            const vnav = this.bus.getSubscriber();
            vnav.on('vnav_altitude_capture_type').whenChanged().handle((v) => {
                this.vnavCaptureType = v;
            });
            this.stateManager.apMasterOn.sub(() => {
                if (this.autopilotInitialized) {
                    this.handleApFdStateChange();
                }
            });
            this.stateManager.isAnyFlightDirectorOn.sub(() => {
                if (this.autopilotInitialized) {
                    this.handleApFdStateChange();
                }
            });
            this.bus.getSubscriber().on('approach_available').handle(available => {
                this.apValues.approachIsActive.set(available);
            });
            if (this.config.defaultMaxBankAngle === undefined) {
                ap.on('ap_max_bank_value').handle(value => {
                    this.apValues.maxBankAngle.set(value);
                });
            }
            if (this.config.defaultMaxNoseUpPitchAngle === undefined) {
                this.bus.getSubscriber().on('ap_set_max_nose_up_pitch').handle(value => {
                    this.apValues.maxNoseUpPitchAngle.set(value);
                });
            }
            if (this.config.defaultMaxNoseDownPitchAngle === undefined) {
                this.bus.getSubscriber().on('ap_set_max_nose_down_pitch').handle(value => {
                    this.apValues.maxNoseDownPitchAngle.set(value);
                });
            }
            if (this.config.publishAutopilotModesAsLVars === true) {
                this.apValues.lateralActive.sub((mode) => SimVar.SetSimVarValue(exports.AutopilotModeVars.LateralActive, exports.SimVarValueType.Number, mode), true);
                this.apValues.lateralArmed.sub((mode) => SimVar.SetSimVarValue(exports.AutopilotModeVars.LateralArmed, exports.SimVarValueType.Number, mode), true);
                this.apValues.verticalActive.sub((mode) => SimVar.SetSimVarValue(exports.AutopilotModeVars.VerticalActive, exports.SimVarValueType.Number, mode), true);
                this.apValues.verticalArmed.sub((mode) => SimVar.SetSimVarValue(exports.AutopilotModeVars.VerticalArmed, exports.SimVarValueType.Number, mode), true);
            }
        }
        /**
         * Overridable method for setting the selected speed values for the A/P to follow.
         */
        monitorApSpeedValues() {
            const ap = this.bus.getSubscriber();
            ap.on('ap_ias_selected').withPrecision(0).handle((ias) => {
                this.apValues.selectedIas.set(ias);
            });
            ap.on('ap_mach_selected').withPrecision(3).handle((mach) => {
                this.apValues.selectedMach.set(mach);
            });
            ap.on('ap_selected_speed_is_mach').whenChanged().handle((inMach) => {
                this.apValues.isSelectedSpeedInMach.set(inMach);
            });
        }
        /**
         * Additional events to be monitored (to be overridden).
         */
        monitorAdditionalEvents() {
            //noop
        }
        /**
         * Manages the FD state and the modes when AP/FD are off.
         */
        handleApFdStateChange() {
            const ap = this.stateManager.apMasterOn.get();
            const fd = this.stateManager.isAnyFlightDirectorOn.get();
            if (ap && !fd && this.config.autoEngageFd !== false) {
                this.stateManager.setFlightDirector(true);
            }
            else if (!ap && !fd) {
                this.lateralModes.forEach((mode) => {
                    if (mode.state !== exports.DirectorState.Inactive) {
                        mode.deactivate();
                    }
                });
                this.verticalModes.forEach((mode) => {
                    if (mode.state !== exports.DirectorState.Inactive) {
                        mode.deactivate();
                    }
                });
                this.apValues.lateralActive.set(exports.APLateralModes.NONE);
                this.apValues.lateralArmed.set(exports.APLateralModes.NONE);
                this.apValues.verticalActive.set(exports.APVerticalModes.NONE);
                this.apValues.verticalArmed.set(exports.APVerticalModes.NONE);
                this.verticalApproachArmed = exports.APVerticalModes.NONE;
                this.verticalAltitudeArmed = exports.APAltitudeModes.NONE;
                this.altCapArmed = false;
            }
        }
        /**
         * Sets a sim AP mode.
         * @param mode The mode to set.
         * @param enabled Whether or not the mode is enabled or disabled.
         */
        setSimAP(mode, enabled) {
            Coherent.call('apSetAutopilotMode', mode, enabled ? 1 : 0);
        }
        /**
         * Checks if the sim AP is in roll mode and sets it if not.
         */
        checkRollModeActive() {
            if (!APController.apGetAutopilotModeActive(exports.MSFSAPStates.Bank)) {
                this.setSimAP(exports.MSFSAPStates.Bank, true);
            }
        }
        /**
         * Checks if the sim AP is in pitch mode and sets it if not.
         */
        checkPitchModeActive() {
            if (!APController.apGetAutopilotModeActive(exports.MSFSAPStates.Pitch)) {
                this.setSimAP(exports.MSFSAPStates.Pitch, true);
            }
        }
        /**
         * Get the default lateral mode from APConfig
         * @returns default lateral mode
         */
        getDefaultLateralMode() {
            if (typeof this.config.defaultLateralMode === 'number') {
                return this.config.defaultLateralMode;
            }
            else {
                return this.config.defaultLateralMode();
            }
        }
        /**
         * Get the default vertical mode from APConfig
         * @returns default vertical mode
         */
        getDefaultVerticalMode() {
            if (typeof this.config.defaultVerticalMode === 'number') {
                return this.config.defaultVerticalMode;
            }
            else {
                return this.config.defaultVerticalMode();
            }
        }
    }

    /**
     * A class that synchronizes LNAV and VNAV data to the sim's built-in GPS system.
     */
    class GpsSynchronizer {
        /**
         * Creates a new instance of GpsSynchronizer.
         * @param bus The event bus.
         * @param flightPlanner The flight planner from which to source active flight plan data.
         * @param facLoader An instance of the facility loader.
         * @param options Options with which to configure the synchronizer.
         */
        constructor(bus, flightPlanner, facLoader, options) {
            var _a, _b;
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.facLoader = facLoader;
            this.magvar = 0;
            this.groundSpeed = 0;
            this.zuluTime = 0;
            this.numPlanLegs = Subject.create(0);
            this.hasReachedDestination = Subject.create(false);
            this.isDestinationLegActive = Subject.create(false);
            this.isDirectToActive = Subject.create(false);
            this.isVNavIndexValid = false;
            this.gpFpa = 0;
            this.gpDeviation = 0;
            this.gpDistance = 0;
            this.isApproachActive = Subject.create(false);
            this.gpAvailable = ConsumerSubject.create(null, false);
            this.publishGpVsr = MappedSubject.create(SubscribableMapFunctions.and(), this.isApproachActive, this.gpAvailable).pause();
            this.vnavVsr = ConsumerSubject.create(null, 0);
            this.gpVsr = ConsumerSubject.create(null, 0);
            this.publishedGpAngleError = undefined;
            this.onDirectToActive = (state) => {
                SimVar.SetSimVarValue('GPS IS DIRECTTO FLIGHTPLAN', exports.SimVarValueType.Bool, state);
            };
            this.onDestinationReached = (state) => {
                SimVar.SetSimVarValue('GPS IS ARRIVED', exports.SimVarValueType.Bool, state);
            };
            this.onApproachActive = (isApproachActive) => {
                SimVar.SetSimVarValue('GPS IS APPROACH ACTIVE', exports.SimVarValueType.Bool, isApproachActive);
            };
            this.lnavIndex = SubscribableUtils.toSubscribable((_a = options === null || options === void 0 ? void 0 : options.lnavIndex) !== null && _a !== void 0 ? _a : 0, true);
            this.vnavIndex = SubscribableUtils.toSubscribable((_b = options === null || options === void 0 ? void 0 : options.vnavIndex) !== null && _b !== void 0 ? _b : 0, true);
            this.initLNav();
            this.initVNav();
            const ahrs = bus.getSubscriber();
            ahrs.on('hdg_deg_true').handle(this.onTrueHeadingChanged.bind(this));
            const gnss = bus.getSubscriber();
            gnss.on('gps-position').handle(this.onPositionChanged.bind(this));
            gnss.on('zulu_time').handle(t => this.zuluTime = t);
            gnss.on('track_deg_true').handle(this.onTrackTrueChanged.bind(this));
            gnss.on('ground_speed').handle(this.onGroundSpeedChanged.bind(this));
            gnss.on('magvar').handle(this.onMagvarChanged.bind(this));
            this.flightPlanner.onEvent('fplActiveLegChange').handle(() => {
                this.hasReachedDestination.set(false);
                if (this.flightPlanner.hasActiveFlightPlan()) {
                    const activeFlightplan = this.flightPlanner.getActiveFlightPlan();
                    this.checkDestinationLegActive(activeFlightplan);
                    this.checkDirectToState(activeFlightplan);
                    this.onIsPrevLegChanged(activeFlightplan);
                    this.onWaypointIndexChanged(activeFlightplan);
                    //PM modif Call that at each leg change otherwise the GPS APPROACH APPROACH TYPE variable is reset (by the sim) when activating an approach
                    this.checkApproachTypeAndTimezone(activeFlightplan, activeFlightplan.procedureDetails.approachIndex);
                    //End PM Modif
                }
            });
            this.flightPlanner.onEvent('fplSegmentChange').handle(this.onPlanChanged.bind(this));
            this.flightPlanner.onEvent('fplIndexChanged').handle(this.onPlanChanged.bind(this));
            this.numPlanLegs.sub(this.onNumLegsChanged.bind(this));
            this.isDirectToActive.sub(this.onDirectToActive, true);
            this.hasReachedDestination.sub(this.onDestinationReached, true);
            this.isApproachActive.sub(this.onApproachActive, true);
        }
        /**
         * Initializes this synchronizer's LNAV subscriptions.
         */
        initLNav() {
            const lnav = this.bus.getSubscriber();
            const dtkHandler = this.onDtkChanged.bind(this);
            const xtkHandler = this.onXtkChanged.bind(this);
            const disHandler = this.onLnavDistanceChanged.bind(this);
            const brgHandler = this.onLnavBearingChanged.bind(this);
            const destDisHandler = this.onLnavDistanceToDestinationChanged.bind(this);
            const courseToSteerHandler = this.onLNavCourseToSteerChanged.bind(this);
            const cdiScaleHandler = this.onCdiScaleChanged.bind(this);
            const lnavSubs = [];
            this.lnavIndex.sub(index => {
                for (const sub of lnavSubs) {
                    sub.destroy();
                }
                lnavSubs.length = 0;
                if (LNavUtils.isValidLNavIndex(index)) {
                    const suffix = LNavUtils.getEventBusTopicSuffix(index);
                    lnavSubs.push(lnav.on(`lnavdata_dtk_mag${suffix}`).whenChanged().handle(dtkHandler), lnav.on(`lnavdata_xtk${suffix}`).whenChanged().handle(xtkHandler), lnav.on(`lnavdata_waypoint_distance${suffix}`).whenChanged().handle(disHandler), lnav.on(`lnavdata_waypoint_bearing_mag${suffix}`).whenChanged().handle(brgHandler), lnav.on(`lnavdata_destination_distance${suffix}`).whenChanged().handle(destDisHandler), lnav.on(`lnav_course_to_steer${suffix}`).whenChanged().handle(courseToSteerHandler), lnav.on(`lnavdata_cdi_scale${suffix}`).whenChanged().handle(cdiScaleHandler));
                }
                else {
                    dtkHandler(0);
                    xtkHandler(0);
                    disHandler(0);
                    brgHandler(0);
                    destDisHandler(0);
                    courseToSteerHandler(null);
                    cdiScaleHandler(0);
                }
            }, true);
        }
        /**
         * Initializes this synchronizer's VNAV and glidepath subscriptions.
         */
        initVNav() {
            const vnavSubs = [];
            const vnav = this.bus.getSubscriber();
            const approachHasGpHandler = this.onApproachHasGpChanged.bind(this);
            const gpFpaHandler = this.onGpFpaChanged.bind(this);
            const gpDeviationHandler = this.onGpDeviationChanged.bind(this);
            const gpDistanceHandler = this.onGpDistanceChanged.bind(this);
            const gsiScaleHandler = this.onGsiScalingChanged.bind(this);
            const targetAltHandler = this.onTargetAltChanged.bind(this);
            const activeLegAltHandler = this.onActiveLegAltChanged.bind(this);
            const vsrHandler = this.onVsrChanged.bind(this);
            const gpAvailableSub = this.gpAvailable.sub(approachHasGpHandler, false, true);
            const vnavVsrSub = this.vnavVsr.sub(vsrHandler, false, true);
            const gpVsrSub = this.gpVsr.sub(vsrHandler, false, true);
            const publishGpVsrSub = this.publishGpVsr.sub(publishGpVsr => {
                if (publishGpVsr) {
                    vnavVsrSub.pause();
                    gpVsrSub.resume(true);
                }
                else {
                    gpVsrSub.pause();
                    vnavVsrSub.resume(true);
                }
            }, false, true);
            this.vnavIndex.sub(index => {
                for (const sub of vnavSubs) {
                    sub.destroy();
                }
                vnavSubs.length = 0;
                this.isVNavIndexValid = VNavUtils.isValidVNavIndex(index);
                if (this.isVNavIndexValid) {
                    const suffix = VNavUtils.getEventBusTopicSuffix(index);
                    vnavSubs.push(vnav.on(`gp_fpa${suffix}`).whenChanged().handle(gpFpaHandler), vnav.on(`gp_vertical_deviation${suffix}`).whenChanged().handle(gpDeviationHandler), vnav.on(`gp_distance${suffix}`).whenChanged().handle(gpDistanceHandler), vnav.on(`gp_gsi_scaling${suffix}`).whenChanged().handle(gsiScaleHandler), vnav.on(`vnav_target_altitude${suffix}`).whenChanged().handle(targetAltHandler), vnav.on(`vnav_active_leg_alt${suffix}`).whenChanged().handle(activeLegAltHandler));
                    this.gpAvailable.setConsumer(vnav.on(`gp_available${suffix}`));
                    gpAvailableSub.resume(true);
                    this.vnavVsr.setConsumer(vnav.on(`vnav_required_vs${suffix}`));
                    this.gpVsr.setConsumer(vnav.on(`gp_required_vs${suffix}`));
                    this.publishGpVsr.resume();
                    publishGpVsrSub.resume(true);
                }
                else {
                    gpAvailableSub.pause();
                    approachHasGpHandler(false);
                    gpFpaHandler(0);
                    gpDeviationHandler(0);
                    gpDistanceHandler(0);
                    gsiScaleHandler(0);
                    targetAltHandler(0);
                    activeLegAltHandler(0);
                    this.gpAvailable.setConsumer(null);
                    this.vnavVsr.setConsumer(null);
                    this.gpVsr.setConsumer(null);
                    this.publishGpVsr.pause();
                    publishGpVsrSub.pause();
                    vnavVsrSub.pause();
                    gpVsrSub.pause();
                    vsrHandler(0);
                }
            }, true);
        }
        /**
         * Updates the GpsSynchronizer.
         */
        update() {
            const isGpsOverridden = SimVar.GetSimVarValue('GPS OVERRIDDEN', exports.SimVarValueType.Bool);
            if (!isGpsOverridden) {
                SimVar.SetSimVarValue('GPS OVERRIDDEN', exports.SimVarValueType.Bool, true);
            }
            let numPlanLegs = 0;
            if (this.flightPlanner.hasActiveFlightPlan()) {
                const plan = this.flightPlanner.getActiveFlightPlan();
                numPlanLegs = plan.length;
            }
            this.numPlanLegs.set(numPlanLegs);
            this.updateGpAngleError();
            //PM Modif fix a bug with GPS IS ACTIVE FLIGHT PLAN that seems to be reset by Asobo
            // when we are changing the active leg and the Asobo flight plan has only an origin
            if (!SimVar.GetSimVarValue('GPS IS ACTIVE FLIGHT PLAN', exports.SimVarValueType.Bool)) {
                const numLegs = this.numPlanLegs.get();
                SimVar.SetSimVarValue('GPS IS ACTIVE FLIGHT PLAN', exports.SimVarValueType.Bool, numLegs > 0);
            }
            //End PM Modif
        }
        /**
         * Updates the current glidepath angle error.
         */
        updateGpAngleError() {
            let valueToPublish;
            if (!this.isVNavIndexValid || this.gpFpa <= 0) {
                valueToPublish = 0;
            }
            else {
                const fpaAltitude = VNavUtils.altitudeForDistance(this.gpFpa, this.gpDistance);
                const calculatedFpaToTarget = VNavUtils.getFpa(this.gpDistance, fpaAltitude + this.gpDeviation);
                valueToPublish = this.gpFpa - calculatedFpaToTarget;
            }
            if (valueToPublish !== this.publishedGpAngleError) {
                this.publishedGpAngleError = valueToPublish;
                SimVar.SetSimVarValue('GPS VERTICAL ANGLE ERROR', exports.SimVarValueType.Degree, valueToPublish);
            }
        }
        /**
         * Handles when the active plan segments are changed.
         */
        onPlanChanged() {
            const plan = this.flightPlanner.getActiveFlightPlan();
            const approachSegments = [...plan.segmentsOfType(exports.FlightPlanSegmentType.Approach)];
            if (approachSegments && approachSegments.length > 0) {
                SimVar.SetSimVarValue('GPS IS APPROACH LOADED', exports.SimVarValueType.Bool, true);
                //SimVar.SetSimVarValue('GPS APPROACH WP COUNT', SimVarValueType.Number, approachSegments[0].legs.length);
            }
            else {
                SimVar.SetSimVarValue('GPS IS APPROACH LOADED', exports.SimVarValueType.Bool, false);
                //SimVar.SetSimVarValue('GPS APPROACH WP COUNT', SimVarValueType.Number, 0);
            }
            //SimVar.SetSimVarValue('GPS APPROACH APPROACH INDEX', SimVarValueType.Number, plan.procedureDetails.approachIndex);
            //SimVar.SetSimVarValue('GPS APPROACH TRANSITION INDEX', SimVarValueType.Number, plan.procedureDetails.approachTransitionIndex);
            this.checkApproachTypeAndTimezone(plan, plan.procedureDetails.approachIndex);
            this.hasReachedDestination.set(false);
            this.checkDestinationLegActive(plan);
            this.checkDirectToState(plan);
            this.onIsPrevLegChanged(plan);
            this.onWaypointIndexChanged(plan);
        }
        /**
         * Handles when the course steered by LNAV changes.
         * @param course The course steered by LNAV, in degrees true.
         */
        onLNavCourseToSteerChanged(course) {
            SimVar.SetSimVarValue('GPS COURSE TO STEER', exports.SimVarValueType.Radians, course ? UnitType.DEGREE.convertTo(course, UnitType.RADIAN) : 0);
        }
        /**
         * Checks to see if we are in a direct to state.
         * @param plan The Active Flight Plan.
         */
        checkDirectToState(plan) {
            let isDirectToActive = false;
            if (plan.activeLateralLeg >= 0 && plan.activeLateralLeg < plan.length) {
                const activeLeg = plan.getLeg(plan.activeLateralLeg);
                isDirectToActive = BitFlags.isAll(activeLeg.flags, exports.LegDefinitionFlags.DirectTo);
            }
            this.isDirectToActive.set(isDirectToActive);
        }
        /**
         * Checks to see if we have reached the plan destination.
         * @param plan The Active Flight Plan
         */
        checkDestinationLegActive(plan) {
            if (plan.length > 1) {
                const finalSegment = plan.getSegment(plan.getSegmentIndex(plan.length - 1));
                const isApproachActive = plan.activeLateralLeg > finalSegment.offset && finalSegment.segmentType === exports.FlightPlanSegmentType.Approach;
                this.isApproachActive.set(isApproachActive);
                let destinationLegIndex = plan.length - 1;
                let fafIndex = -1;
                if (isApproachActive) {
                    for (let i = finalSegment.legs.length - 1; i >= 0; i--) {
                        const leg = finalSegment.legs[i];
                        if (!BitFlags.isAll(leg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                            destinationLegIndex = i + finalSegment.offset;
                        }
                        if (leg.leg.fixTypeFlags === exports.FixTypeFlags.FAF) {
                            fafIndex = i + finalSegment.offset;
                            break;
                        }
                    }
                }
                this.checkApproachMode(plan, isApproachActive, fafIndex);
                if (!this.hasReachedDestination.get() && destinationLegIndex === plan.activeLateralLeg) {
                    this.isDestinationLegActive.set(true);
                    return;
                }
            }
            else {
                this.checkApproachMode(plan, false, -1);
            }
            this.isDestinationLegActive.set(false);
        }
        /**
         * Checks the approach mode on leg change.
         * @param plan The Active Flight Plan.
         * @param isApproachActive Whether the approach is active.
         * @param fafIndex The destination leg index.
         */
        checkApproachMode(plan, isApproachActive, fafIndex) {
            let approachMode = 0;
            let currentLeg;
            if (isApproachActive && plan.activeLateralLeg >= 0 && plan.activeLateralLeg < plan.length) {
                currentLeg = plan.getLeg(plan.activeLateralLeg);
                if (BitFlags.isAll(currentLeg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                    approachMode = 3;
                }
                else if (fafIndex > -1 && plan.activeLateralLeg >= fafIndex) {
                    approachMode = 2;
                }
                else {
                    approachMode = 1;
                }
            }
            this.checkApproachWaypointType(currentLeg);
            SimVar.SetSimVarValue('GPS APPROACH MODE', exports.SimVarValueType.Number, approachMode);
            SimVar.SetSimVarValue('GPS APPROACH IS FINAL', exports.SimVarValueType.Bool, approachMode === 2);
        }
        /**
         * Handles when the active leg index changes.
         * @param plan The Active Flight Plan.
         */
        onWaypointIndexChanged(plan) {
            var _a;
            let name = '';
            if (plan.activeLateralLeg >= 0 && plan.activeLateralLeg < plan.length) {
                const leg = plan.getLeg(plan.activeLateralLeg);
                name = (_a = leg.name) !== null && _a !== void 0 ? _a : '';
                if (leg === null || leg === void 0 ? void 0 : leg.calculated) {
                    SimVar.SetSimVarValue('GPS WP NEXT LAT', exports.SimVarValueType.Degree, leg.calculated.endLat);
                    SimVar.SetSimVarValue('GPS WP NEXT LON', exports.SimVarValueType.Degree, leg.calculated.endLon);
                }
            }
            SimVar.SetSimVarValue('GPS WP NEXT ID', exports.SimVarValueType.String, name);
        }
        /**
         * Handles when the number of active plan legs changes.
         * @param numLegs The number of active plan legs.
         */
        onNumLegsChanged(numLegs) {
            SimVar.SetSimVarValue('GPS IS ACTIVE FLIGHT PLAN', exports.SimVarValueType.Bool, numLegs > 0);
            SimVar.SetSimVarValue('GPS IS ACTIVE WAY POINT', exports.SimVarValueType.Bool, (this.isDirectToActive.get() || numLegs > 1));
            //SimVar.SetSimVarValue('GPS FLIGHT PLAN WP COUNT', SimVarValueType.Number, numLegs);
            if (this.flightPlanner.hasActiveFlightPlan()) {
                const plan = this.flightPlanner.getActiveFlightPlan();
                this.onIsPrevLegChanged(plan);
            }
        }
        /**
         * Handles when the previous leg changes.
         * @param plan The Active Flight Plan
         */
        onIsPrevLegChanged(plan) {
            var _a;
            const numLegs = this.numPlanLegs.get();
            let name = '';
            if (numLegs > 1 && plan.activeLateralLeg > 0 && plan.activeLateralLeg < plan.length) {
                const prevLeg = plan.getLeg(plan.activeLateralLeg - 1);
                if (!FlightPlanUtils.isDiscontinuityLeg(prevLeg.leg.type)) {
                    SimVar.SetSimVarValue('GPS WP PREV VALID', exports.SimVarValueType.Bool, true);
                    name = (_a = prevLeg.name) !== null && _a !== void 0 ? _a : '';
                    if (prevLeg.calculated) {
                        SimVar.SetSimVarValue('GPS WP PREV LAT', exports.SimVarValueType.Degree, prevLeg.calculated.endLat);
                        SimVar.SetSimVarValue('GPS WP PREV LON', exports.SimVarValueType.Degree, prevLeg.calculated.endLon);
                    }
                }
            }
            SimVar.SetSimVarValue('GPS WP PREV ID', exports.SimVarValueType.String, name);
        }
        /**
         * Handles when the LNAV Distance to Destination Changes.
         * @param dis The new distance to destination.
         */
        onLnavDistanceToDestinationChanged(dis) {
            const eteSeconds = this.groundSpeed > 1 ? 3600 * dis / this.groundSpeed : 0;
            if (isNaN(eteSeconds)) {
                return;
            }
            SimVar.SetSimVarValue('GPS ETE', exports.SimVarValueType.Seconds, eteSeconds);
            SimVar.SetSimVarValue('GPS ETA', exports.SimVarValueType.Seconds, eteSeconds + this.zuluTime);
        }
        /**
         * Handles when the LNAV DTK changes.
         * @param dtk The new DTK.
         */
        onDtkChanged(dtk) {
            SimVar.SetSimVarValue('GPS WP DESIRED TRACK', exports.SimVarValueType.Radians, UnitType.DEGREE.convertTo(dtk, UnitType.RADIAN));
        }
        /**
         * Handles when the LNAV XTK changes.
         * @param xtk The new XTK.
         */
        onXtkChanged(xtk) {
            SimVar.SetSimVarValue('GPS WP CROSS TRK', exports.SimVarValueType.Meters, UnitType.NMILE.convertTo(xtk, UnitType.METER) * -1);
        }
        /**
         * Handles when the LNAV DIS to WP changes.
         * @param dis The distance.
         */
        onLnavDistanceChanged(dis) {
            if (this.isDestinationLegActive.get() && Math.abs(dis) < 2) {
                this.hasReachedDestination.set(true);
            }
            const distanceMeters = UnitType.NMILE.convertTo(dis, UnitType.METER);
            SimVar.SetSimVarValue('GPS WP DISTANCE', exports.SimVarValueType.Meters, distanceMeters);
            const eteSeconds = this.groundSpeed > 1 ? 3600 * dis / this.groundSpeed : 0;
            SimVar.SetSimVarValue('GPS WP ETE', exports.SimVarValueType.Seconds, eteSeconds);
            SimVar.SetSimVarValue('GPS WP ETA', exports.SimVarValueType.Seconds, eteSeconds + this.zuluTime);
        }
        /**
         * Handles when the LNAV Bearing to WP changes.
         * @param brg The bearing.
         */
        onLnavBearingChanged(brg) {
            SimVar.SetSimVarValue('GPS WP BEARING', exports.SimVarValueType.Radians, UnitType.DEGREE.convertTo(brg, UnitType.RADIAN));
        }
        /**
         * Handles when the True Ground Track Changes.
         * @param trk The true track.
         */
        onTrackTrueChanged(trk) {
            SimVar.SetSimVarValue('GPS GROUND TRUE TRACK', exports.SimVarValueType.Radians, UnitType.DEGREE.convertTo(trk, UnitType.RADIAN));
        }
        /**
         * Handles when the Ground Speed changes.
         * @param gs The current ground speed.
         */
        onGroundSpeedChanged(gs) {
            this.groundSpeed = gs;
            SimVar.SetSimVarValue('GPS GROUND SPEED', exports.SimVarValueType.MetersPerSecond, UnitType.KNOT.convertTo(gs, UnitType.MPS));
        }
        /**
         * Handles when the true heading changes.
         * @param hdg The true heading.
         */
        onTrueHeadingChanged(hdg) {
            SimVar.SetSimVarValue('GPS GROUND TRUE HEADING', exports.SimVarValueType.Radians, UnitType.DEGREE.convertTo(hdg, UnitType.RADIAN));
        }
        /**
         * Handles when the magvar changes.
         * @param magvar The new magvar.
         */
        onMagvarChanged(magvar) {
            this.magvar = magvar;
            SimVar.SetSimVarValue('GPS MAGVAR', exports.SimVarValueType.Radians, UnitType.DEGREE.convertTo(magvar, UnitType.RADIAN));
        }
        /**
         * Handles when the plane position changes.
         * @param pos The new plane position.
         */
        onPositionChanged(pos) {
            SimVar.SetSimVarValue('GPS POSITION LAT', exports.SimVarValueType.Degree, pos.lat);
            SimVar.SetSimVarValue('GPS POSITION LON', exports.SimVarValueType.Degree, pos.long);
            SimVar.SetSimVarValue('GPS POSITION ALT', exports.SimVarValueType.Meters, pos.alt);
        }
        /**
         * Handles when the GPS CDI scale changes.
         * @param scaleNm The scale, in nautical miles.
         */
        onCdiScaleChanged(scaleNm) {
            SimVar.SetSimVarValue('GPS CDI SCALING', exports.SimVarValueType.Meters, UnitType.NMILE.convertTo(scaleNm, UnitType.METER));
        }
        /**
         * Handles when whether the loaded approach has glidepath guidance changes.
         * @param approachHasGp Whether the loaded approach has glidepath guidance.
         */
        onApproachHasGpChanged(approachHasGp) {
            SimVar.SetSimVarValue('GPS HAS GLIDEPATH', exports.SimVarValueType.Bool, approachHasGp);
        }
        /**
         * Handles when the glidepath angle changes.
         * @param fpa The new glidepath angle, in degrees.
         */
        onGpFpaChanged(fpa) {
            this.gpFpa = fpa;
            SimVar.SetSimVarValue('GPS VERTICAL ANGLE', exports.SimVarValueType.Degree, fpa);
        }
        /**
         * Handles when the glidepath deviation changes.
         * @param deviation The new deviation, in feet.
         */
        onGpDeviationChanged(deviation) {
            this.gpDeviation = deviation;
            const deviationMeters = UnitType.FOOT.convertTo(deviation, UnitType.METER);
            SimVar.SetSimVarValue('GPS VERTICAL ERROR', exports.SimVarValueType.Meters, -deviationMeters);
        }
        /**
         * Handles when the distance to the glidepath endpoint changes.
         * @param dis The new distance to the glidepath endpoint, in feet.
         */
        onGpDistanceChanged(dis) {
            this.gpDistance = dis;
        }
        /**
         * Handles when the glidepath deviation scale changes.
         * @param gsiScaling The new scale, in feet.
         */
        onGsiScalingChanged(gsiScaling) {
            const gsiScalingMeters = UnitType.FOOT.convertTo(gsiScaling, UnitType.METER);
            SimVar.SetSimVarValue('GPS GSI SCALING', exports.SimVarValueType.Meters, gsiScalingMeters);
        }
        /**
         * Handles when the VNAV target altitude changes.
         * @param targetAlt The new target altitude, in feet (can be -1 if none is defined or available)
         */
        onTargetAltChanged(targetAlt) {
            SimVar.SetSimVarValue('GPS TARGET ALTITUDE', exports.SimVarValueType.Meters, targetAlt > 0 ? UnitType.FOOT.convertTo(targetAlt, UnitType.METER) : 0);
        }
        /**
         * Handles when the VNAV active leg altitude changes.
         * @param alt The new active leg altitude, in meters.
         */
        onActiveLegAltChanged(alt) {
            SimVar.SetSimVarValue('GPS WP NEXT ALT', exports.SimVarValueType.Meters, alt > 0 ? alt : 0);
        }
        /**
         * Handles when the vertical speed required changes.
         * @param vs The new required vertical speed, in feet per minute.
         */
        onVsrChanged(vs) {
            SimVar.SetSimVarValue('GPS WP VERTICAL SPEED', exports.SimVarValueType.MetersPerSecond, UnitType.FPM.convertTo(vs, UnitType.MPS));
        }
        /**
         * Handles checking the approach type and timezone.
         * @param plan The active flight plan.
         * @param approachIndex The approach index in the active plan.
         */
        async checkApproachTypeAndTimezone(plan, approachIndex) {
            let approachType = 0;
            if (plan.getUserData('visual_approach') !== undefined) {
                approachType = ApproachType.APPROACH_TYPE_RNAV;
            }
            else if (approachIndex > -1 && plan.destinationAirport) {
                const facility = await this.facLoader.getFacility(exports.FacilityType.Airport, plan.destinationAirport);
                approachType = facility.approaches[approachIndex].approachType;
                if (approachType === exports.AdditionalApproachType.APPROACH_TYPE_VISUAL) {
                    approachType = ApproachType.APPROACH_TYPE_RNAV;
                }
                // TODO: Find a way to get the timezone from the facility or by lat/lon?
            }
            SimVar.SetSimVarValue('GPS APPROACH APPROACH TYPE', exports.SimVarValueType.Number, approachType);
        }
        /**
         * Handles checking the approach waypoint type.
         * @param leg The active lateral leg.
         */
        checkApproachWaypointType(leg) {
            let legType = 0;
            let segmentType = 0;
            if (leg) {
                switch (leg.leg.type) {
                    case exports.LegType.AF:
                        legType = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 4 : 5;
                        segmentType = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 2 : 1;
                        break;
                    case exports.LegType.RF:
                        legType = 1;
                        segmentType = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 2 : 1;
                        break;
                    case exports.LegType.CA:
                    case exports.LegType.FA:
                    case exports.LegType.VA:
                        legType = 9;
                        break;
                    case exports.LegType.FM:
                    case exports.LegType.VM:
                        legType = 10;
                        break;
                    case exports.LegType.CD:
                    case exports.LegType.FD:
                    case exports.LegType.VD:
                        legType = 8;
                        break;
                    case exports.LegType.PI:
                        legType = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 2 : 3;
                        break;
                    case exports.LegType.HA:
                    case exports.LegType.HM:
                    case exports.LegType.HF:
                        legType = leg.leg.turnDirection === exports.LegTurnDirection.Left ? 6 : 7;
                        break;
                    default:
                        legType = 1;
                }
            }
            SimVar.SetSimVarValue('GPS APPROACH WP TYPE', exports.SimVarValueType.Number, legType);
            SimVar.SetSimVarValue('GPS APPROACH SEGMENT TYPE', exports.SimVarValueType.Number, segmentType);
            SimVar.SetSimVarValue('GPS APPROACH IS WP RUNWAY', exports.SimVarValueType.Bool, (leg === null || leg === void 0 ? void 0 : leg.leg.fixIcao[0]) === 'R');
        }
    }

    /**
     * Autothrottle target modes.
     */
    exports.AutothrottleTargetMode = void 0;
    (function (AutothrottleTargetMode) {
        /** No target. */
        AutothrottleTargetMode["None"] = "None";
        /** Autothrottle targets a specific airspeed. */
        AutothrottleTargetMode["Speed"] = "Speed";
        /** Autothrottle targets a specific engine power setting. */
        AutothrottleTargetMode["Power"] = "Power";
        /** Autothrottle targets a specific throttle lever position. */
        AutothrottleTargetMode["ThrottlePos"] = "ThrottlePos";
    })(exports.AutothrottleTargetMode || (exports.AutothrottleTargetMode = {}));

    /**
     * A record of an autothrottle speed target.
     */
    class AutothrottleSpeedTargetRecord {
        constructor() {
            /** The target indicated airspeed, in knots, or `null` if there is no targeted indicated airspeed. */
            this.ias = null;
            /** The target mach number, or `null` if there is no targeted mach number. */
            this.mach = null;
            /** The conversion factor from mach number to indicated airspeed (knots), or `null` if the factor is unavailable. */
            this.machToKias = null;
        }
        /**
         * Copies this record from another one.
         * @param other The record to copy from.
         */
        copyFrom(other) {
            this.ias = other.ias;
            this.mach = other.mach;
            this.machToKias = other.machToKias;
        }
        /**
         * Resets this record. All properties will be set to `null`.
         */
        reset() {
            this.ias = null;
            this.mach = null;
            this.machToKias = null;
        }
    }
    /**
     * An abstract implementation of an autothrottle system.
     *
     * The system contains a global speed controller and one power controller for each engine throttle. The speed
     * controller commands a synced engine power target for all throttles based on over/underspeed protection and the
     * selected speed target, if active. Each power controller commands a position for its individual throttle based on
     * overpower protection and power target, either from the speed controller or the selected power target, if active.
     */
    class AbstractAutothrottle {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param airspeedIndex The index of the sim airspeed indicator from which this autothrottle should source airspeed
         * data.
         * @param throttleInfos Information on the throttles controlled by this autothrottle. Each entry in the array should
         * describe a single unique throttle. Order does not matter.
         * @param options Options with which to initialize this autothrottle.
         * @param throttleLeverManager The throttle lever manager to use to request position changes for virtual throttle
         * levers. If not defined, position changes for virtual throttle levers will be requested using key events
         * (specifically the `THROTTLE[N]_SET` event).
         */
        constructor(bus, airspeedIndex, throttleInfos, options, throttleLeverManager) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.isOverspeedProtActive = Subject.create(false);
            this.isUnderspeedProtActive = Subject.create(false);
            this.isOverpowerProtActive = Subject.create(false);
            this.targetMode = Subject.create(exports.AutothrottleTargetMode.None);
            this.selectedSpeedIsMach = Subject.create(false);
            this.selectedIas = Subject.create(0);
            this.selectedMach = Subject.create(0);
            this.selectedPower = Subject.create(0);
            this.selectedThrottlePos = Subject.create(0);
            this.maxIas = Subject.create(0);
            this.minIas = Subject.create(0);
            this.maxMach = Subject.create(0);
            this.minMach = Subject.create(0);
            this.maxPower = Subject.create(0);
            this.maxThrottlePos = Subject.create(1);
            this.minThrottlePos = Subject.create(0);
            this.lastSmoothedIas = undefined;
            this.selectedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.overspeedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.underspeedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.prevSelectedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.prevOverspeedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.prevUnderspeedSpeedTarget = new AutothrottleSpeedTargetRecord();
            this.realTime = ConsumerSubject.create(this.bus.getSubscriber().on('realTime'), 0);
            this.updateTimer = null;
            this.lastUpdateTime = 0;
            this.updateHandler = this.update.bind(this);
            this.speedCommand = {
                selectedSpeedPowerTarget: undefined,
                overspeedProtPowerTarget: undefined,
                underspeedProtPowerTarget: undefined,
                isOverspeed: false,
                isUnderspeed: false
            };
            this.powerCommand = {
                speed: undefined,
                targetPos: undefined,
                isOverspeedProtEngaged: false,
                isUnderspeedProtEngaged: false,
                isOverpowerProtEngaged: false
            };
            this.prevProtectionStates = {
                isOverspeedProtEngaged: false,
                isUnderspeedProtEngaged: false
            };
            this.isAlive = true;
            this.useIndicatedMach = (_a = options.useIndicatedMach) !== null && _a !== void 0 ? _a : false;
            this.airspeedIndex = SubscribableUtils.toSubscribable(airspeedIndex, true);
            this.airspeedIndex.sub(index => { this.airspeedSimVar = `AIRSPEED INDICATED:${index}`; }, true);
            this.powerLookahead = SubscribableUtils.toSubscribable(options.powerLookahead, true);
            this.throttles = throttleInfos.map(info => {
                return this.createThrottle(bus, info, options.servoSpeed, options.powerSmoothingConstant, options.powerSmoothingVelocityConstant, this.powerLookahead, options.powerLookaheadSmoothingConstant, options.powerLookaheadSmoothingVelocityConstant, throttleLeverManager);
            });
            this.machToKiasSmoother = new ExpSmoother(options.speedSmoothingConstant);
            this.iasLookahead = SubscribableUtils.toSubscribable(options.speedLookahead, true);
            this.iasSmoother = new MultiExpSmoother(options.speedSmoothingConstant, options.speedSmoothingVelocityConstant);
            this.lookaheadIasSmoother = new MultiExpSmoother((_b = options.speedLookaheadSmoothingConstant) !== null && _b !== void 0 ? _b : options.speedSmoothingConstant, (_c = options.speedLookaheadSmoothingVelocityConstant) !== null && _c !== void 0 ? _c : options.speedSmoothingVelocityConstant);
            this.lastIasLookahead = this.iasLookahead.get();
            this.shouldTargetAccel = options.selectedSpeedAccelTarget !== undefined;
            this.selectedSpeedAccelTargetFunc = options.selectedSpeedAccelTarget;
            this.overspeedAccelTargetFunc = (_d = options.overspeedAccelTarget) !== null && _d !== void 0 ? _d : options.selectedSpeedAccelTarget;
            this.underspeedAccelTargetFunc = (_e = options.underspeedAccelTarget) !== null && _e !== void 0 ? _e : options.selectedSpeedAccelTarget;
            if (this.shouldTargetAccel) {
                const accelSmoothingConstant = (_f = options.accelSmoothingConstant) !== null && _f !== void 0 ? _f : 0;
                this.accelSmoother = new MultiExpSmoother(accelSmoothingConstant, options.accelSmoothingVelocityConstant);
                this.selectedSpeedAccelCorrectionSmoother = new MultiExpSmoother(accelSmoothingConstant, options.accelSmoothingVelocityConstant);
                this.overspeedAccelCorrectionSmoother = new MultiExpSmoother(accelSmoothingConstant, options.accelSmoothingVelocityConstant);
                this.underspeedAccelCorrectionSmoother = new MultiExpSmoother(accelSmoothingConstant, options.accelSmoothingVelocityConstant);
                const accelTargetTau = (_g = options.accelTargetSmoothingConstant) !== null && _g !== void 0 ? _g : 0;
                this.selectedSpeedAccelTargetSmoother = new ExpSmoother(accelTargetTau);
                this.overspeedProtAccelTargetSmoother = new ExpSmoother(accelTargetTau);
                this.underspeedProtAccelTargetSmoother = new ExpSmoother(accelTargetTau);
            }
            this.selectedSpeedPid = AbstractAutothrottle.createPidFromParams(options.speedTargetPid);
            this.overspeedPid = AbstractAutothrottle.createPidFromParams((_h = options.overspeedPid) !== null && _h !== void 0 ? _h : options.speedTargetPid);
            this.underspeedPid = AbstractAutothrottle.createPidFromParams((_j = options.underspeedPid) !== null && _j !== void 0 ? _j : options.speedTargetPid);
            this.selectedPowerPids = {};
            this.overpowerPids = {};
            this.throttleSpeedSmoothers = {};
            this.speedTargetChangeThreshold = (_k = options.speedTargetChangeThreshold) !== null && _k !== void 0 ? _k : Infinity;
            this.overspeedChangeThreshold = (_l = options.overspeedChangeThreshold) !== null && _l !== void 0 ? _l : Infinity;
            this.underspeedChangeThreshold = (_m = options.underspeedChangeThreshold) !== null && _m !== void 0 ? _m : Infinity;
            this.selectedSpeedPowerTargetSmoother = new ExpSmoother(options.powerTargetSmoothingConstant);
            this.overspeedProtPowerTargetSmoother = new ExpSmoother(options.powerTargetSmoothingConstant);
            this.underspeedProtPowerTargetSmoother = new ExpSmoother(options.powerTargetSmoothingConstant);
            this.selectedSpeedPowerCorrectionTransformer = (_o = options.selectedSpeedPowerCorrectionTransformer) !== null && _o !== void 0 ? _o : AbstractAutothrottle.defaultPowerCorrectionTransformer;
            this.overspeedPowerCorrectionTransformer = (_p = options.overspeedPowerCorrectionTransformer) !== null && _p !== void 0 ? _p : this.selectedSpeedPowerCorrectionTransformer;
            this.underspeedPowerCorrectionTransformer = (_q = options.underspeedPowerCorrectionTransformer) !== null && _q !== void 0 ? _q : this.selectedSpeedPowerCorrectionTransformer;
            const latchOptions = Object.assign({}, options.latchOptions);
            (_r = latchOptions.latchThreshold) !== null && _r !== void 0 ? _r : (latchOptions.latchThreshold = -1);
            (_s = latchOptions.latchDebounce) !== null && _s !== void 0 ? _s : (latchOptions.latchDebounce = 0);
            latchOptions.unlatchThreshold = Math.max((_t = latchOptions.unlatchThreshold) !== null && _t !== void 0 ? _t : 0, latchOptions.latchThreshold);
            (_u = latchOptions.unlatchDebounce) !== null && _u !== void 0 ? _u : (latchOptions.unlatchDebounce = 0);
            this.latchOptions = latchOptions;
            this.latchRecord = {};
            this.hysteresis = Math.max(0, options.hysteresis);
            this.hysteresisRecord = {};
            for (const index of AbstractAutothrottle.ALL_THROTTLE_INDEXES) {
                this.selectedPowerPids[index] = AbstractAutothrottle.createPidFromParams(options.powerTargetPid);
                this.overpowerPids[index] = AbstractAutothrottle.createPidFromParams((_v = options.overpowerPid) !== null && _v !== void 0 ? _v : options.powerTargetPid);
                this.throttleSpeedSmoothers[index] = new ExpSmoother((_w = options.throttleSpeedSmoothingConstant) !== null && _w !== void 0 ? _w : 0);
                this.latchRecord[index] = 0;
                this.hysteresisRecord[index] = 0;
            }
            // Publish data
            this.publisher.pub('at_master_is_active', false, true, true);
            this.isOverspeedProtActive.sub(val => this.publisher.pub('at_overspeed_prot_is_active', val, true, true), true);
            this.isUnderspeedProtActive.sub(val => this.publisher.pub('at_underspeed_prot_is_active', val, true, true), true);
            this.isOverpowerProtActive.sub(val => this.publisher.pub('at_overpower_prot_is_active', val, true, true), true);
            this.targetMode.sub(val => this.publisher.pub('at_target_mode', val, true, true), true);
            this.selectedIas.sub(val => this.publisher.pub('at_selected_ias', val, true, true), true);
            this.selectedMach.sub(val => this.publisher.pub('at_selected_mach', val, true, true), true);
            this.selectedSpeedIsMach.sub(val => this.publisher.pub('at_selected_speed_is_mach', val, true, true), true);
            this.selectedPower.sub(val => this.publisher.pub('at_selected_power', val, true, true), true);
            this.selectedThrottlePos.sub(val => this.publisher.pub('at_selected_throttle_pos', val, true, true), true);
            this.maxIas.sub(val => this.publisher.pub('at_max_ias', val, true, true), true);
            this.maxMach.sub(val => this.publisher.pub('at_max_mach', val, true, true), true);
            this.minIas.sub(val => this.publisher.pub('at_min_ias', val, true, true), true);
            this.minMach.sub(val => this.publisher.pub('at_min_mach', val, true, true), true);
            this.maxPower.sub(val => this.publisher.pub('at_max_power', val, true, true), true);
            this.maxThrottlePos.sub(val => this.publisher.pub('at_max_throttle_pos', val, true, true), true);
            this.minThrottlePos.sub(val => this.publisher.pub('at_min_throttle_pos', val, true, true), true);
            this.publisher.pub('at_overspeed_prot_is_engaged', false, true);
            this.publisher.pub('at_underspeed_prot_is_engaged', false, true);
        }
        /**
         * Sets whether this autothrottle's overspeed protection is active.
         * @param val Whether overspeed protection is active.
         */
        setOverspeedProtActive(val) {
            this.isOverspeedProtActive.set(val);
        }
        /**
         * Sets whether this autothrottle's underspeed protection is active.
         * @param val Whether underspeed protection is active.
         */
        setUnderspeedProtActive(val) {
            this.isUnderspeedProtActive.set(val);
        }
        /**
         * Sets whether this autothrottle's overpower protection is active.
         * @param val Whether overpower protection is active.
         */
        setOverpowerProtActive(val) {
            this.isOverpowerProtActive.set(val);
        }
        /**
         * Sets this autothrottle's target mode.
         * @param mode A target mode.
         */
        setTargetMode(mode) {
            this.targetMode.set(mode);
        }
        /**
         * Sets whether this autothrottle's selected speed target is a mach number.
         * @param val Whether the selected speed target is a mach number.
         */
        setSelectedSpeedIsMach(val) {
            this.selectedSpeedIsMach.set(val);
        }
        /**
         * Sets this autothrottle's selected indicated airspeed target, in knots.
         * @param ias An indicated airspeed, in knots.
         */
        setSelectedIas(ias) {
            this.selectedIas.set(ias);
        }
        /**
         * Sets this autothrottle's selected mach number target.
         * @param mach A mach number.
         */
        setSelectedMach(mach) {
            this.selectedMach.set(mach);
        }
        /**
         * Sets this autothrottle's selected engine power target.
         * @param power An engine power value.
         */
        setSelectedPower(power) {
            this.selectedPower.set(power);
        }
        /**
         * Sets this autothrottle's selected normalized throttle lever position target.
         * @param pos A normalized throttle lever position.
         */
        setSelectedThrottlePos(pos) {
            this.selectedThrottlePos.set(pos);
        }
        /**
         * Sets this autothrottle's maximum allowed indicated airspeed, in knots.
         * @param ias An indicated airspeed, in knots.
         */
        setMaxIas(ias) {
            this.maxIas.set(ias);
        }
        /**
         * Sets this autothrottle's minimum allowed indicated airspeed, in knots.
         * @param ias An indicated airspeed, in knots.
         */
        setMinIas(ias) {
            this.minIas.set(ias);
        }
        /**
         * Sets this autothrottle's maximum allowed mach number.
         * @param mach A mach number.
         */
        setMaxMach(mach) {
            this.maxMach.set(mach);
        }
        /**
         * Sets this autothrottle's minimum allowed mach number.
         * @param mach A mach number.
         */
        setMinMach(mach) {
            this.minMach.set(mach);
        }
        /**
         * Sets this autothrottle's maximum allowed engine power.
         * @param power An engine power value.
         */
        setMaxPower(power) {
            this.maxPower.set(power);
        }
        /**
         * Sets this autothrottle's maximum allowed normalized throttle lever position.
         * @param pos A normalized throttle lever position.
         */
        setMaxThrottlePos(pos) {
            this.maxThrottlePos.set(pos);
        }
        /**
         * Sets this autothrottle's minimum allowed normalized throttle lever position.
         * @param pos A normalized throttle lever position.
         */
        setMinThrottlePos(pos) {
            this.minThrottlePos.set(pos);
        }
        /**
         * Sets whether one of this autothrottle's throttle servos are active.
         * @param index The index of the throttle servo to activate/deactivate.
         * @param active Whether the servo should be activated.
         */
        setServoActive(index, active) {
            const throttle = this.throttles.find(query => query.index === index);
            if (throttle !== undefined) {
                throttle.isServoActive = active;
            }
        }
        /**
         * Turns this autothrottle on with a specified update frequency. If this autothrottle is already running, then it
         * will be turned off before turning on again with the specified frequency.
         * @param frequency The frequency, in hertz, at which this autothrottle will update.
         * @throws Error if this autothrottle has been destroyed.
         */
        start(frequency) {
            if (!this.isAlive) {
                throw new Error('AbstractAutothrottle: cannot start a dead autothrottle');
            }
            this.stop();
            this.publisher.pub('at_master_is_active', true, true, true);
            this.updateTimer = setInterval(this.updateHandler, 1000 / frequency);
        }
        /**
         * Turns this autothrottle off.
         * @throws Error if this autothrottle has been destroyed.
         */
        stop() {
            var _a, _b, _c, _d, _e, _f, _g;
            if (!this.isAlive) {
                throw new Error('AbstractAutothrottle: cannot stop a dead autothrottle');
            }
            if (this.updateTimer === null) {
                return;
            }
            clearInterval(this.updateTimer);
            this.updateTimer = null;
            this.machToKiasSmoother.reset();
            this.iasSmoother.reset();
            this.lookaheadIasSmoother.reset();
            this.lastSmoothedIas = undefined;
            (_a = this.accelSmoother) === null || _a === void 0 ? void 0 : _a.reset();
            (_b = this.selectedSpeedAccelCorrectionSmoother) === null || _b === void 0 ? void 0 : _b.reset();
            (_c = this.overspeedAccelCorrectionSmoother) === null || _c === void 0 ? void 0 : _c.reset();
            (_d = this.underspeedAccelCorrectionSmoother) === null || _d === void 0 ? void 0 : _d.reset();
            (_e = this.selectedSpeedAccelTargetSmoother) === null || _e === void 0 ? void 0 : _e.reset();
            (_f = this.overspeedProtAccelTargetSmoother) === null || _f === void 0 ? void 0 : _f.reset();
            (_g = this.underspeedProtAccelTargetSmoother) === null || _g === void 0 ? void 0 : _g.reset();
            this.selectedSpeedPid.reset();
            this.overspeedPid.reset();
            this.underspeedPid.reset();
            this.prevSelectedSpeedTarget.reset();
            this.prevOverspeedSpeedTarget.reset();
            this.prevUnderspeedSpeedTarget.reset();
            this.selectedSpeedPowerTargetSmoother.reset();
            this.overspeedProtPowerTargetSmoother.reset();
            this.underspeedProtPowerTargetSmoother.reset();
            for (let i = 0; i < this.throttles.length; i++) {
                this.throttles[i].resetPowerSmoothing();
            }
            for (const index of AbstractAutothrottle.ALL_THROTTLE_INDEXES) {
                this.selectedPowerPids[index].reset();
                this.overpowerPids[index].reset();
                this.throttleSpeedSmoothers[index].reset();
                this.latchRecord[index] = 0;
                this.hysteresisRecord[index] = 0;
            }
            this.publisher.pub('at_master_is_active', false, true, true);
        }
        /**
         * Updates this autothrottle.
         */
        update() {
            const realTime = Date.now();
            const dt = (realTime - this.lastUpdateTime) / 1000;
            this.lastUpdateTime = realTime;
            // This shouldn't really ever happen, but just in case...
            if (dt <= 0) {
                return;
            }
            // Check if the current time has diverged from the event bus value by more than 1 second.
            // If it has, we are probably paused in the menu and should skip the update.
            if (realTime - this.realTime.get() >= 1000) {
                return;
            }
            // Update power and position of all throttles
            for (let i = 0; i < this.throttles.length; i++) {
                this.throttles[i].update(dt);
            }
            const targetMode = this.targetMode.get();
            // Obtain power target (if any) commanded by the speed controller.
            const speedCommand = this.calculateSpeedTargetPower(dt, this.speedCommand);
            const isOverpowerProtActive = this.isOverpowerProtActive.get();
            const isPowerTargetActive = targetMode === exports.AutothrottleTargetMode.Power;
            const isThrottlePosTargetActive = targetMode === exports.AutothrottleTargetMode.ThrottlePos;
            const minThrottlePos = this.minThrottlePos.get();
            const maxThrottlePos = this.maxThrottlePos.get();
            let isUnderspeedProtEngaged = false;
            let isOverspeedProtEngaged = false;
            for (let i = 0; i < this.throttles.length; i++) {
                const throttle = this.throttles[i];
                if (throttle.isServoActive) {
                    // Obtain throttle position adjustment commanded by the power controller.
                    const powerCommand = this.calculatePowerTargetThrottlePos(throttle, speedCommand, isOverpowerProtActive, isPowerTargetActive, isThrottlePosTargetActive, dt, this.powerCommand);
                    let targetPos = powerCommand.targetPos;
                    let speed = powerCommand.speed;
                    const isThrottlePosOob = throttle.normPosition < minThrottlePos || throttle.normPosition > maxThrottlePos;
                    const isTargetPosOob = targetPos !== undefined && (targetPos < minThrottlePos || targetPos > maxThrottlePos);
                    if (isTargetPosOob) {
                        // If the commanded throttle lever position is out of bounds, clamp it to within bounds.
                        targetPos = MathUtils.clamp(targetPos, minThrottlePos, maxThrottlePos);
                        speed = (targetPos - throttle.normPosition) / dt;
                    }
                    else if (isThrottlePosOob && targetPos === undefined) {
                        // If there is no commanded throttle lever position but the current throttle lever position is out of
                        // bounds, command the throttle lever to move back in bounds.
                        targetPos = MathUtils.clamp(throttle.normPosition, minThrottlePos, maxThrottlePos);
                        speed = (targetPos - throttle.normPosition) / dt;
                    }
                    if (targetPos !== undefined && speed !== undefined) {
                        let shouldDriveThrottle = true;
                        // Check throttle latch.
                        if ((!isThrottlePosTargetActive
                            || powerCommand.isOverpowerProtEngaged
                            || powerCommand.isOverspeedProtEngaged
                            || powerCommand.isUnderspeedProtEngaged)
                            && powerCommand.speed !== undefined
                            && this.latchOptions.latchThreshold >= 0) {
                            if (this.latchRecord[throttle.index] < this.latchOptions.latchDebounce) {
                                // The throttle is not latched. Check if the throttle lever position is not out of bounds and the
                                // difference between the position and the commanded position does not exceed the latch threshold. If so,
                                // then increment the latch timer. Otherwise, reset the latch timer.
                                if (!isThrottlePosOob && Math.abs(powerCommand.speed) <= this.latchOptions.latchThreshold) {
                                    this.latchRecord[throttle.index] = Math.min(this.latchRecord[throttle.index] + dt, this.latchOptions.latchDebounce);
                                }
                                else {
                                    this.latchRecord[throttle.index] = 0;
                                }
                            }
                            else {
                                // The throttle is latched. Check if the throttle lever position is out of bounds or if the difference
                                // between the position and the commanded position has exceeded the unlatch threshold for the required
                                // debounce delay. If so, then unlatch the throttle. Otherwise, prevent the throttle lever from moving.
                                if (isThrottlePosOob) {
                                    this.latchRecord[throttle.index] = 0;
                                }
                                else if (Math.abs(powerCommand.speed) >= this.latchOptions.unlatchThreshold) {
                                    if ((this.latchRecord[throttle.index] += dt) >= this.latchOptions.latchDebounce + this.latchOptions.unlatchDebounce) {
                                        this.latchRecord[throttle.index] = 0;
                                    }
                                    else {
                                        shouldDriveThrottle = false;
                                    }
                                }
                                else {
                                    this.latchRecord[throttle.index] = this.latchOptions.latchDebounce;
                                    shouldDriveThrottle = false;
                                }
                            }
                        }
                        if (shouldDriveThrottle) {
                            // Check hysteresis, unless the current throttle position is out of bounds, in which case we always want to
                            // move the throttle back in bounds.
                            const lastCommandedSpeed = this.hysteresisRecord[throttle.index];
                            if (isThrottlePosOob
                                || lastCommandedSpeed === 0
                                || Math.sign(lastCommandedSpeed) === Math.sign(speed)
                                || Math.abs(speed) > this.hysteresis) {
                                this.hysteresisRecord[throttle.index] = speed;
                            }
                            else {
                                shouldDriveThrottle = false;
                            }
                            if (shouldDriveThrottle) {
                                throttle.drive(targetPos, dt);
                            }
                        }
                        else {
                            this.hysteresisRecord[throttle.index] = 0;
                        }
                    }
                    else {
                        this.latchRecord[throttle.index] = 0;
                        this.hysteresisRecord[throttle.index] = 0;
                    }
                    if (powerCommand.isOverspeedProtEngaged) {
                        isOverspeedProtEngaged = true;
                    }
                    else if (powerCommand.isUnderspeedProtEngaged) {
                        isUnderspeedProtEngaged = true;
                    }
                }
                else {
                    this.selectedPowerPids[throttle.index].reset();
                    this.overpowerPids[throttle.index].reset();
                    this.throttleSpeedSmoothers[throttle.index].reset();
                    this.hysteresisRecord[throttle.index] = 0;
                    this.latchRecord[throttle.index] = 0;
                }
            }
            if (isOverspeedProtEngaged !== this.prevProtectionStates.isOverspeedProtEngaged) {
                this.publisher.pub('at_overspeed_prot_is_engaged', isOverspeedProtEngaged, true);
                this.prevProtectionStates.isOverspeedProtEngaged = isOverspeedProtEngaged;
            }
            if (isUnderspeedProtEngaged !== this.prevProtectionStates.isUnderspeedProtEngaged) {
                this.publisher.pub('at_underspeed_prot_is_engaged', isUnderspeedProtEngaged, true);
                this.prevProtectionStates.isUnderspeedProtEngaged = isUnderspeedProtEngaged;
            }
        }
        /**
         * Calculates the engine power for all throttles commanded by this autothrottle's speed controller. The speed
         * controller incorporates the speed target and over-/under-speed protection if active.
         * @param dt The elapsed time since the last update.
         * @param out The object to which to write the results.
         * @returns The engine power for all throttles commanded by this autothrottle's speed controller.
         */
        calculateSpeedTargetPower(dt, out) {
            var _a, _b, _c, _d, _e, _f;
            out.selectedSpeedPowerTarget = undefined;
            out.overspeedProtPowerTarget = undefined;
            out.underspeedProtPowerTarget = undefined;
            out.isOverspeed = false;
            out.isUnderspeed = false;
            const pressure = SimVar.GetSimVarValue('AMBIENT PRESSURE', exports.SimVarValueType.HPA);
            const ias = SimVar.GetSimVarValue(this.airspeedSimVar, exports.SimVarValueType.Knots);
            let currentMachToKias;
            if (this.useIndicatedMach) {
                if (ias > 1) {
                    const iasMps = UnitType.KNOT.convertTo(ias, UnitType.MPS);
                    currentMachToKias = ias / AeroMath.casToMach(iasMps, pressure);
                }
                else {
                    currentMachToKias = 1 / AeroMath.casToMach(1, pressure);
                }
            }
            else {
                const mach = SimVar.GetSimVarValue('AIRSPEED MACH', exports.SimVarValueType.Mach);
                currentMachToKias = ias > 1 && mach > 0 ? ias / mach : 1 / AeroMath.casToMach(1, pressure);
            }
            const machToKias = this.machToKiasSmoother.next(isFinite(currentMachToKias) ? currentMachToKias : 1, dt);
            const lookahead = Math.max(0, this.iasLookahead.get());
            const smoothedIas = this.iasSmoother.next(ias, dt);
            if (lookahead !== this.lastIasLookahead) {
                this.lookaheadIasSmoother.reset();
                this.lastIasLookahead = lookahead;
            }
            let effectiveIas;
            const deltaIas = smoothedIas - (this.lastSmoothedIas === undefined ? smoothedIas : this.lastSmoothedIas);
            if (lookahead > 0 && this.lastSmoothedIas !== undefined) {
                // Somehow, NaN values can creep in here. So we will make sure if that happens we don't leave the system in an
                // unrecoverable state by resetting the smoother when the last smoothed value is NaN.
                const last = this.lookaheadIasSmoother.last();
                effectiveIas = last === null || isFinite(last)
                    ? this.lookaheadIasSmoother.next(ias + deltaIas * lookahead / dt, dt)
                    : this.lookaheadIasSmoother.reset(ias + deltaIas * lookahead / dt);
            }
            else {
                effectiveIas = smoothedIas;
            }
            this.lastSmoothedIas = smoothedIas;
            let prevEffectiveAccel = null;
            let effectiveAccel = 0;
            if (this.shouldTargetAccel) {
                prevEffectiveAccel = this.accelSmoother.last();
                effectiveAccel = this.accelSmoother.next(deltaIas / dt, dt);
            }
            const isTargetSpeed = this.targetMode.get() === exports.AutothrottleTargetMode.Speed;
            const isOverspeedProtActive = this.isOverspeedProtActive.get();
            const isUnderspeedProtActive = this.isUnderspeedProtActive.get();
            let overspeedProtDelta;
            let underspeedProtDelta;
            let selectedSpeedDelta;
            if (isOverspeedProtActive) {
                this.overspeedSpeedTarget.ias = this.maxIas.get();
                this.overspeedSpeedTarget.mach = this.maxMach.get();
                this.overspeedSpeedTarget.machToKias = machToKias;
                if (this.shouldTargetAccel) {
                    overspeedProtDelta = this.updateAccelTargetPid(this.overspeedPid, this.overspeedAccelTargetFunc, this.overspeedAccelCorrectionSmoother, this.overspeedProtAccelTargetSmoother, this.overspeedPowerCorrectionTransformer, effectiveAccel, prevEffectiveAccel, effectiveIas, this.overspeedSpeedTarget, this.prevOverspeedSpeedTarget, false, dt, this.overspeedChangeThreshold);
                }
                else {
                    overspeedProtDelta = this.updateSpeedTargetPid(this.overspeedPid, this.overspeedPowerCorrectionTransformer, effectiveIas, this.overspeedSpeedTarget, this.prevOverspeedSpeedTarget, false, dt, this.overspeedChangeThreshold);
                }
                out.isOverspeed = effectiveIas > Math.min(this.overspeedSpeedTarget.mach * machToKias, this.overspeedSpeedTarget.ias);
                this.prevOverspeedSpeedTarget.copyFrom(this.overspeedSpeedTarget);
            }
            else {
                this.overspeedPid.reset();
                (_a = this.overspeedAccelCorrectionSmoother) === null || _a === void 0 ? void 0 : _a.reset();
                (_b = this.overspeedProtAccelTargetSmoother) === null || _b === void 0 ? void 0 : _b.reset();
                this.prevOverspeedSpeedTarget.reset();
            }
            if (isUnderspeedProtActive) {
                this.underspeedSpeedTarget.ias = this.minIas.get();
                this.underspeedSpeedTarget.mach = this.minMach.get();
                this.underspeedSpeedTarget.machToKias = machToKias;
                if (this.shouldTargetAccel) {
                    underspeedProtDelta = this.updateAccelTargetPid(this.underspeedPid, this.underspeedAccelTargetFunc, this.underspeedAccelCorrectionSmoother, this.underspeedProtAccelTargetSmoother, this.underspeedPowerCorrectionTransformer, effectiveAccel, prevEffectiveAccel, effectiveIas, this.underspeedSpeedTarget, this.prevUnderspeedSpeedTarget, true, dt, this.underspeedChangeThreshold);
                }
                else {
                    underspeedProtDelta = this.updateSpeedTargetPid(this.underspeedPid, this.underspeedPowerCorrectionTransformer, effectiveIas, this.underspeedSpeedTarget, this.prevUnderspeedSpeedTarget, true, dt, this.underspeedChangeThreshold);
                }
                out.isUnderspeed = effectiveIas < Math.max(this.underspeedSpeedTarget.mach * machToKias, this.underspeedSpeedTarget.ias);
                this.prevUnderspeedSpeedTarget.copyFrom(this.underspeedSpeedTarget);
            }
            else {
                this.underspeedPid.reset();
                (_c = this.underspeedAccelCorrectionSmoother) === null || _c === void 0 ? void 0 : _c.reset();
                (_d = this.underspeedProtAccelTargetSmoother) === null || _d === void 0 ? void 0 : _d.reset();
                this.underspeedSpeedTarget.reset();
            }
            if (isTargetSpeed) {
                // Targeting speed
                if (this.selectedSpeedIsMach.get()) {
                    this.selectedSpeedTarget.ias = null;
                    this.selectedSpeedTarget.mach = this.selectedMach.get();
                }
                else {
                    this.selectedSpeedTarget.ias = this.selectedIas.get();
                    this.selectedSpeedTarget.mach = null;
                }
                this.selectedSpeedTarget.machToKias = machToKias;
                if (this.shouldTargetAccel) {
                    selectedSpeedDelta = this.updateAccelTargetPid(this.selectedSpeedPid, this.selectedSpeedAccelTargetFunc, this.selectedSpeedAccelCorrectionSmoother, this.selectedSpeedAccelTargetSmoother, this.selectedSpeedPowerCorrectionTransformer, effectiveAccel, prevEffectiveAccel, effectiveIas, this.selectedSpeedTarget, this.prevSelectedSpeedTarget, false, dt, this.speedTargetChangeThreshold);
                }
                else {
                    selectedSpeedDelta = this.updateSpeedTargetPid(this.selectedSpeedPid, this.selectedSpeedPowerCorrectionTransformer, effectiveIas, this.selectedSpeedTarget, this.prevSelectedSpeedTarget, false, dt, this.speedTargetChangeThreshold);
                }
                this.prevSelectedSpeedTarget.copyFrom(this.selectedSpeedTarget);
            }
            else {
                this.selectedSpeedPid.reset();
                (_e = this.selectedSpeedAccelCorrectionSmoother) === null || _e === void 0 ? void 0 : _e.reset();
                (_f = this.selectedSpeedAccelTargetSmoother) === null || _f === void 0 ? void 0 : _f.reset();
                this.prevSelectedSpeedTarget.reset();
            }
            let throttlePowerSum = 0;
            let throttlePowerCount = 0;
            for (let i = 0; i < this.throttles.length; i++) {
                const throttle = this.throttles[i];
                if (throttle.isServoActive) {
                    throttlePowerSum += throttle.effectivePower;
                    throttlePowerCount++;
                }
            }
            if (throttlePowerCount === 0) {
                this.overspeedProtPowerTargetSmoother.reset();
                this.underspeedProtPowerTargetSmoother.reset();
                this.selectedSpeedPowerTargetSmoother.reset();
                return out;
            }
            if (selectedSpeedDelta !== undefined) {
                out.selectedSpeedPowerTarget = this.selectedSpeedPowerTargetSmoother.next(throttlePowerSum / throttlePowerCount + selectedSpeedDelta, dt);
            }
            else {
                this.selectedSpeedPowerTargetSmoother.reset();
            }
            if (overspeedProtDelta !== undefined) {
                out.overspeedProtPowerTarget = this.overspeedProtPowerTargetSmoother.next(throttlePowerSum / throttlePowerCount + overspeedProtDelta, dt);
            }
            else {
                this.overspeedProtPowerTargetSmoother.reset();
            }
            if (underspeedProtDelta !== undefined) {
                out.underspeedProtPowerTarget = this.underspeedProtPowerTargetSmoother.next(throttlePowerSum / throttlePowerCount + underspeedProtDelta, dt);
            }
            else {
                this.underspeedProtPowerTargetSmoother.reset();
            }
            return out;
        }
        /**
         * Updates a speed PID controller when directly targeting airspeed.
         * @param pid The PID controller to update.
         * @param correctionTransformer A function which transforms the power target correction calculated by the speed PID
         * controller.
         * @param effectiveIas The airplane's current effective indicated airspeed, in knots.
         * @param currentSpeedTarget The current target speed.
         * @param prevSpeedTarget The target speed during the previous update.
         * @param useMaxSpeed If `true`, then the greater of the target indicated airspeed or target mach will be used if
         * both are defined. If `false`, then the lesser of the target indicated airspeed or target mach will be used if both
         * are defined.
         * @param dt The elapsed time since the last update, in seconds.
         * @param targetChangeThreshold The threshold, in knots per second, such that if the rate of change of the target
         * speed exceeds the threshold, the PID controller will ignore any contribution to the derivative term from the
         * changing target speed.
         * @returns The output of the speed PID controller.
         */
        updateSpeedTargetPid(pid, correctionTransformer, effectiveIas, currentSpeedTarget, prevSpeedTarget, useMaxSpeed, dt, targetChangeThreshold) {
            var _a, _b, _c, _d;
            const targetSpeedFunc = useMaxSpeed ? Math.max : Math.min;
            const currentTargetIas = targetSpeedFunc((_a = currentSpeedTarget.ias) !== null && _a !== void 0 ? _a : Infinity, ((_b = currentSpeedTarget.mach) !== null && _b !== void 0 ? _b : Infinity) * currentSpeedTarget.machToKias);
            const error = currentTargetIas - effectiveIas;
            // Cancel out any derivative term contributed by changes in the target speed.
            const prevTargetIasAtCurrentUpdate = targetSpeedFunc((_c = prevSpeedTarget.ias) !== null && _c !== void 0 ? _c : Infinity, ((_d = prevSpeedTarget.mach) !== null && _d !== void 0 ? _d : Infinity) * currentSpeedTarget.machToKias);
            if (isFinite(prevTargetIasAtCurrentUpdate)) {
                const targetIasDelta = currentTargetIas - prevTargetIasAtCurrentUpdate;
                const lastError = pid.getPreviousError();
                if (lastError !== undefined && Math.abs(targetIasDelta / dt) >= targetChangeThreshold) {
                    pid.getOutput(0, lastError + targetIasDelta);
                }
            }
            return correctionTransformer(pid.getOutput(dt, error), error, currentTargetIas, effectiveIas);
        }
        /**
         * Updates a speed PID controller when targeting acceleration.
         * @param pid The PID controller to update.
         * @param accelTargetFunc A function which generates an acceleration to target, in knots per second, for a given
         * airspeed error.
         * @param accelCorrectionSmoother The smoother to use to smooth the target acceleration correction.
         * @param accelTargetSmoother The smoother to use to smooth the target acceleration.
         * @param correctionTransformer A function which transforms the power target correction calculated by the speed PID
         * controller.
         * @param effectiveAccel The airplane's effective indicated acceleration, in knots per second.
         * @param prevEffectiveAccel The airplane's effective indicated acceleration during the previous update, in knots per
         * second.
         * @param effectiveIas The airplane's current effective indicated airspeed, in knots.
         * @param currentSpeedTarget The current target speed.
         * @param prevSpeedTarget The target speed during the previous update.
         * @param useMaxSpeed If `true`, then the greater of the target indicated airspeed or target mach will be used if
         * both are defined. If `false`, then the lesser of the target indicated airspeed or target mach will be used if both
         * are defined.
         * @param dt The elapsed time since the last update, in seconds.
         * @param targetChangeThreshold The threshold, in knots per second, such that if the rate of change of the target
         * speed exceeds the threshold, the PID controller will ignore any contribution to the derivative term from the
         * changing target speed.
         * @returns The output of the speed PID controller.
         */
        updateAccelTargetPid(pid, accelTargetFunc, accelCorrectionSmoother, accelTargetSmoother, correctionTransformer, effectiveAccel, prevEffectiveAccel, effectiveIas, currentSpeedTarget, prevSpeedTarget, useMaxSpeed, dt, targetChangeThreshold) {
            var _a, _b, _c, _d, _e, _f;
            const targetSpeedFunc = useMaxSpeed ? Math.max : Math.min;
            const currentTargetIas = targetSpeedFunc((_a = currentSpeedTarget.ias) !== null && _a !== void 0 ? _a : Infinity, ((_b = currentSpeedTarget.mach) !== null && _b !== void 0 ? _b : Infinity) * currentSpeedTarget.machToKias);
            // Correct for any change in target IAS due to changing conditions (e.g. target IAS will increase or decrease with
            // static pressure if derived from a constant targeted mach).
            let accelCorrection = 0;
            if (prevSpeedTarget.machToKias !== null) {
                const currentTargetIasAtPrevUpdate = targetSpeedFunc((_c = currentSpeedTarget.ias) !== null && _c !== void 0 ? _c : Infinity, ((_d = currentSpeedTarget.mach) !== null && _d !== void 0 ? _d : Infinity) * prevSpeedTarget.machToKias);
                accelCorrection = accelCorrectionSmoother.next((currentTargetIas - currentTargetIasAtPrevUpdate) / dt, dt);
            }
            else {
                accelCorrectionSmoother.reset();
            }
            const iasError = currentTargetIas - effectiveIas;
            const targetAccel = accelTargetSmoother.next(accelTargetFunc(iasError, currentTargetIas, effectiveIas, effectiveAccel, accelCorrection), dt);
            // Cancel out any derivative term contributed by changes in the target speed.
            if (prevEffectiveAccel !== null && pid.getPreviousError() !== undefined) {
                const prevTargetIasAtCurrentUpdate = targetSpeedFunc((_e = prevSpeedTarget.ias) !== null && _e !== void 0 ? _e : Infinity, ((_f = prevSpeedTarget.mach) !== null && _f !== void 0 ? _f : Infinity) * currentSpeedTarget.machToKias);
                if (isFinite(prevTargetIasAtCurrentUpdate)) {
                    const targetIasDelta = currentTargetIas - prevTargetIasAtCurrentUpdate;
                    if (Math.abs(targetIasDelta / dt) >= targetChangeThreshold) {
                        pid.getOutput(0, targetAccel - prevEffectiveAccel);
                    }
                }
            }
            const error = targetAccel - effectiveAccel;
            return correctionTransformer(pid.getOutput(dt, error), iasError, currentTargetIas, effectiveIas);
        }
        /**
         * Calculates the throttle position for a specific throttle commanded by this autothrottle's power controller. The
         * power controller incorporates the power target if active and overpower protection.
         * @param throttle The throttle for which to calculate commanded throttle position.
         * @param speedCommand The engine power commanded by this autothrottle's speed controller.
         * @param isOverpowerProtActive Whether overpower protection is active.
         * @param isPowerTargetActive Whether power target is active.
         * @param isThrottlePosTargetActive Whether throttle lever position target is active.
         * @param dt The elapsed time since the last update.
         * @param out The object to which to write the results.
         * @returns The throttle position for the specified throttle commanded by this autothrottle's power controller.
         */
        calculatePowerTargetThrottlePos(throttle, speedCommand, isOverpowerProtActive, isPowerTargetActive, isThrottlePosTargetActive, dt, out) {
            out.speed = undefined;
            out.targetPos = undefined;
            out.isOverspeedProtEngaged = false;
            out.isUnderspeedProtEngaged = false;
            out.isOverpowerProtEngaged = false;
            const targetPid = this.selectedPowerPids[throttle.index];
            const overpowerPid = this.overpowerPids[throttle.index];
            const power = throttle.power;
            const effectivePower = throttle.effectivePower;
            let overpowerProtDelta;
            let isOverpower = false;
            if (isOverpowerProtActive) {
                const maxPower = this.maxPower.get();
                overpowerProtDelta = overpowerPid.getOutput(dt, maxPower - effectivePower);
                isOverpower = power > maxPower;
            }
            else {
                overpowerPid.reset();
            }
            let targetDelta;
            let delta;
            let isUsingOverspeedProtCommand = false;
            let isUsingUnderspeedProtCommand = false;
            let isUsingOverpowerProtCommand = false;
            let powerTarget;
            if (isPowerTargetActive) {
                powerTarget = this.selectedPower.get();
            }
            else {
                powerTarget = speedCommand.selectedSpeedPowerTarget;
            }
            let selectedThrottlePositionPowerTargetDelta;
            if (isThrottlePosTargetActive) {
                const selectedTarget = this.selectedThrottlePos.get();
                const servoSpeed = throttle.servoSpeed.get();
                selectedThrottlePositionPowerTargetDelta = MathUtils.clamp((selectedTarget - throttle.normPosition) / dt, -servoSpeed, servoSpeed);
            }
            if (powerTarget === undefined) {
                // The autothrottle is not targeting selected speed or selected power. Engage over/underspeed protection if the
                // airplane is currently over/underspeeding and the protection is attempting to reduce power (overspeed)
                // increase power (underspeed). If neither protection can be engaged in this way, then check if a selected
                // throttle position is being targeted. If so, then engage over/underspeed protection if either is active (with
                // arbitrary preference for overspeed protection). Selected throttle position will get a chance to "override" the
                // protection further down in this method.
                if (speedCommand.overspeedProtPowerTarget !== undefined
                    && speedCommand.isOverspeed
                    && speedCommand.overspeedProtPowerTarget < effectivePower) {
                    isUsingOverspeedProtCommand = true;
                }
                else if (speedCommand.underspeedProtPowerTarget !== undefined
                    && speedCommand.isUnderspeed
                    && speedCommand.underspeedProtPowerTarget > effectivePower) {
                    isUsingUnderspeedProtCommand = true;
                }
                else if (selectedThrottlePositionPowerTargetDelta !== undefined) {
                    if (speedCommand.overspeedProtPowerTarget !== undefined) {
                        isUsingOverspeedProtCommand = true;
                    }
                    else if (speedCommand.underspeedProtPowerTarget !== undefined) {
                        isUsingUnderspeedProtCommand = true;
                    }
                }
                if (isUsingOverspeedProtCommand) {
                    powerTarget = speedCommand.overspeedProtPowerTarget;
                }
                else if (isUsingUnderspeedProtCommand) {
                    powerTarget = speedCommand.underspeedProtPowerTarget;
                }
            }
            else {
                // The autothrottle is targeting a selected speed or selected power. Engage over/underspeed protection if the
                // protection is commanding a power target less than (overspeed) or greater than (underspeed) that commanded by
                // the selected speed or selected power controller.
                if (speedCommand.overspeedProtPowerTarget !== undefined && speedCommand.overspeedProtPowerTarget < powerTarget) {
                    powerTarget = speedCommand.overspeedProtPowerTarget;
                    isUsingOverspeedProtCommand = true;
                }
                else if (speedCommand.underspeedProtPowerTarget !== undefined && speedCommand.underspeedProtPowerTarget > powerTarget) {
                    powerTarget = speedCommand.underspeedProtPowerTarget;
                    isUsingUnderspeedProtCommand = true;
                }
            }
            if (powerTarget !== undefined) {
                // Targeting power
                targetDelta = targetPid.getOutput(dt, powerTarget - effectivePower);
            }
            else {
                targetPid.reset();
            }
            if (targetDelta === undefined) {
                // No power target is defined. Engage overpower protection if the airplane is currently in an overpower condition
                // and the protection is attempting to reduce power OR if a selected throttle position is being targeted.
                // Selected throttle position will get a chance to "override" the protection further down in this method.
                if (overpowerProtDelta !== undefined && (isThrottlePosTargetActive || (isOverpower && overpowerProtDelta < 0))) {
                    delta = overpowerProtDelta;
                    isUsingOverpowerProtCommand = true;
                }
            }
            else {
                // A power target is defined. Engage overpower protection if the protection is targeting a lower power than the
                // existing power target.
                if (overpowerProtDelta !== undefined && overpowerProtDelta < targetDelta) {
                    delta = overpowerProtDelta;
                    isUsingOverpowerProtCommand = true;
                }
                else {
                    delta = targetDelta;
                }
            }
            if (delta !== undefined) {
                delta = this.throttleSpeedSmoothers[throttle.index].next(delta, dt);
            }
            else {
                this.throttleSpeedSmoothers[throttle.index].reset();
            }
            if (selectedThrottlePositionPowerTargetDelta !== undefined) {
                // Override the throttle adjustment commanded by the power controller with the adjustment commanded by the
                // selected throttle position if...
                if (
                // ... the power controller is not commanding any adjustment
                delta === undefined
                    // ... OR overspeed or overpower protection is engaged but the adjustment required to move toward the selected
                    // throttle position would reduce throttle *lower* than that commanded by the power controller
                    || ((isUsingOverspeedProtCommand || isUsingOverpowerProtCommand) && selectedThrottlePositionPowerTargetDelta < delta)
                    // ... OR underspeed protection is engaged but the adjustment required to move toward the selected throttle
                    // position would increase throttle *higher* than that commanded by the power controller
                    || (isUsingUnderspeedProtCommand && selectedThrottlePositionPowerTargetDelta > delta)) {
                    delta = selectedThrottlePositionPowerTargetDelta;
                    isUsingOverspeedProtCommand = false;
                    isUsingUnderspeedProtCommand = false;
                    isUsingOverpowerProtCommand = false;
                }
            }
            if (delta === undefined) {
                return out;
            }
            out.speed = delta;
            out.targetPos = throttle.normPosition + delta * dt;
            out.isOverspeedProtEngaged = isUsingOverspeedProtCommand;
            out.isUnderspeedProtEngaged = isUsingUnderspeedProtCommand;
            out.isOverpowerProtEngaged = isUsingOverpowerProtCommand;
            return out;
        }
        /**
         * Destroys this autothrottle.
         */
        destroy() {
            this.isAlive = false;
            this.stop();
            this.realTime.destroy();
            this.throttles.forEach(throttle => { throttle.destroy(); });
        }
        /**
         * Creates a PID controller using a given set of parameters.
         * @param params A set of PID parameters.
         * @returns A new PID controller created using the specified parameters.
         */
        static createPidFromParams(params) {
            return new PidController(params.kP, params.kI, params.kD, params.maxOut, params.minOut, params.maxI, params.minI);
        }
        /**
         * A default transformation function for power target corrections which returns the correction unchanged.
         * @param correction The raw power target correction.
         * @returns The transformed power target correction.
         */
        static defaultPowerCorrectionTransformer(correction) {
            return correction;
        }
    }
    AbstractAutothrottle.ALL_THROTTLE_INDEXES = [1, 2, 3, 4];
    /**
     * A throttle controlled by an autothrottle system.
     */
    class AutothrottleThrottle {
        // eslint-disable-next-line jsdoc/require-returns
        /** The current position of this throttle. */
        get position() {
            return this._position;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /**
         * The position of this throttle, normalized such that `0` is the idle position and `1` is the maximum thrust
         * position.
         */
        get normPosition() {
            return (this._position - this.idlePosition) / this.normRange;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The power delivered by this throttle's engine. */
        get power() {
            return this._power;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** The effective power delivered by this throttle's engine, after smoothing and lookahead have been applied. */
        get effectivePower() {
            return this._effectivePower;
        }
        // eslint-disable-next-line jsdoc/require-returns
        /** Whether the autothrottle servo for this throttle is active. */
        get isServoActive() {
            return this._isServoActive.get();
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set isServoActive(val) {
            this._isServoActive.set(val);
        }
        /**
         * Constructor.
         * @param bus The event bus.
         * @param info Information describing this throttle.
         * @param servoSpeed The speed delivered by the servo controlling this throttle, in units of normalized position per
         * second.
         * @param powerSmoothingConstant The smoothing time constant, in seconds, to use to smooth engine power data.
         * @param powerSmoothingVelocityConstant The smoothing time constant, in seconds, to use to smooth estimated power
         * velocity while smoothing engine power data. A value of zero is equivalent to no smoothing. If not defined,
         * estimated power velocity will not be used to adjust smoothed engine power data.
         * @param powerLookahead The lookahead time, in seconds, to use for engine power data.
         * @param powerLookaheadSmoothingConstant The smoothing time constant, in seconds, to use to smooth lookahead engine
         * power data. If not defined, defaults to the value of {@linkcode powerSmoothingConstant}.
         * @param powerLookaheadSmoothingVelocityConstant The smoothing time constant, in seconds, to use to smooth estimated
         * power velocity while smoothing lookahead engine power data. A value of zero is equivalent to no smoothing. If not
         * defined, estimated power velocity will not be used to adjust smoothed lookahead engine power data. If not defined,
         * defaults to the value of {@linkcode powerSmoothingVelocityConstant}.
         * @param throttleLeverManager The throttle lever manager to use to request position changes for this throttle's
         * lever. If not defined, position changes for the lever will be requested using key events (specifically the
         * `THROTTLE[N]_SET` event).
         */
        constructor(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager) {
            this.powerLookahead = powerLookahead;
            this._position = 0;
            this._power = 0;
            this._effectivePower = 0;
            this._isServoActive = Subject.create(false);
            this.lastPowerLookahead = this.powerLookahead.get();
            this.lastSmoothedPower = undefined;
            this.servoSpeed = typeof servoSpeed === 'number' ? Value.create(servoSpeed) : servoSpeed;
            this.initKeyManager(bus);
            ({ index: this.index, idlePosition: this.idlePosition, maxThrustPosition: this.maxThrustPosition } = info);
            if (info.isVirtual) {
                this.virtualPos = ConsumerSubject.create(bus.getSubscriber().on(`v_throttle_lever_pos_${this.index}`), 0),
                    this.getPosition = () => {
                        return this.virtualPos.get();
                    };
            }
            else {
                this.throttlePosSimVar = `GENERAL ENG THROTTLE LEVER POSITION:${this.index}`;
                this.getPosition = () => {
                    return SimVar.GetSimVarValue(this.throttlePosSimVar, exports.SimVarValueType.Percent) / 100;
                };
            }
            this.normRange = this.maxThrustPosition - this.idlePosition;
            this.throttleSetKVar = `THROTTLE${this.index}_SET`;
            this.throttleLeverManager = info.isVirtual ? throttleLeverManager : undefined;
            const isServoActiveTopic = `at_servo_${this.index}_is_active`;
            this._isServoActive.sub(val => { bus.getPublisher().pub(isServoActiveTopic, val, true, true); });
            this.powerSmoother = new MultiExpSmoother(powerSmoothingConstant, powerSmoothingVelocityConstant);
            this.lookaheadPowerSmoother = new MultiExpSmoother(powerLookaheadSmoothingConstant !== null && powerLookaheadSmoothingConstant !== void 0 ? powerLookaheadSmoothingConstant : powerSmoothingConstant, powerLookaheadSmoothingVelocityConstant !== null && powerLookaheadSmoothingVelocityConstant !== void 0 ? powerLookaheadSmoothingVelocityConstant : powerSmoothingVelocityConstant);
        }
        /**
         * Initializes the key event manager used by this throttle.
         * @param bus The event bus.
         */
        async initKeyManager(bus) {
            this.keyEventManager = await KeyEventManager.getManager(bus);
        }
        /**
         * Updates this throttle's current position and delivered power properties.
         * @param dt The elapsed time since the last update, in seconds.
         */
        update(dt) {
            this._position = this.getPosition();
            this._power = this.getPower();
            const lookahead = Math.max(0, this.powerLookahead.get());
            const smoothedPower = this.powerSmoother.next(this._power, dt);
            if (lookahead !== this.lastPowerLookahead) {
                this.lookaheadPowerSmoother.reset();
                this.lastPowerLookahead = lookahead;
            }
            if (lookahead > 0 && this.lastSmoothedPower !== undefined) {
                const delta = smoothedPower - this.lastSmoothedPower;
                this._effectivePower = this.lookaheadPowerSmoother.next(this._power + delta * lookahead / dt, dt);
            }
            else {
                this._effectivePower = smoothedPower;
            }
            this.lastSmoothedPower = smoothedPower;
        }
        /**
         * Drives this throttle toward a target normalized position over a period of time.
         * @param targetNormPos The target normalized position. Will be clamped to the range `[0, 1]`.
         * @param dt The amount of time over which to drive the throttle, in seconds.
         */
        drive(targetNormPos, dt) {
            var _a;
            const current = this.normPosition;
            const delta = targetNormPos - current;
            if (delta === 0) {
                return;
            }
            const deltaSign = Math.sign(delta);
            const toDrive = Math.min(dt * this.servoSpeed.get(), (targetNormPos - current) * deltaSign) * deltaSign;
            const finalPos = this.idlePosition + (current + toDrive) * this.normRange;
            if (Math.abs(finalPos - this._position) < 0.5 / AutothrottleThrottle.RAW_AXIS_MAX) {
                return;
            }
            if (this.throttleLeverManager !== undefined) {
                this.throttleLeverManager.setThrottleLeverPosRaw(this.index, finalPos * AutothrottleThrottle.RAW_AXIS_MAX);
            }
            else {
                (_a = this.keyEventManager) === null || _a === void 0 ? void 0 : _a.triggerKey(this.throttleSetKVar, false, Math.round(finalPos * AutothrottleThrottle.RAW_AXIS_MAX));
            }
        }
        /**
         * Resets this throttle's power smoothing.
         */
        resetPowerSmoothing() {
            this.powerSmoother.reset();
            this.lookaheadPowerSmoother.reset();
            this.lastSmoothedPower = undefined;
        }
        /**
         * Destroys this throttle.
         */
        destroy() {
            var _a;
            (_a = this.virtualPos) === null || _a === void 0 ? void 0 : _a.destroy();
        }
    }
    AutothrottleThrottle.RAW_AXIS_MAX = 16384;

    /**
     * An autothrottle system for turbine jet engines.
     */
    class JetAutothrottle extends AbstractAutothrottle {
        /** @inheritdoc */
        createThrottle(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager) {
            return new JetAutothrottleThrottle(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager);
        }
    }
    /**
     * An autothrottle throttle for turbine jet engines.
     */
    class JetAutothrottleThrottle extends AutothrottleThrottle {
        /** @inheritdoc */
        constructor(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager) {
            super(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager);
            this.commandedN1SimVar = `TURB ENG THROTTLE COMMANDED N1:${this.index}`;
        }
        /** @inheritdoc */
        getPower() {
            return SimVar.GetSimVarValue(this.commandedN1SimVar, exports.SimVarValueType.Percent);
        }
    }

    /**
     * An autothrottle system for turboprop engines.
     */
    class TurbopropAutothrottle extends AbstractAutothrottle {
        /** @inheritdoc */
        createThrottle(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager) {
            return new TurbopropAutothrottleThrottle(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager);
        }
    }
    /**
     * An autothrottle throttle for turboprop engines.
     */
    class TurbopropAutothrottleThrottle extends AutothrottleThrottle {
        /** @inheritdoc */
        constructor(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager) {
            super(bus, info, servoSpeed, powerSmoothingConstant, powerSmoothingVelocityConstant, powerLookahead, powerLookaheadSmoothingConstant, powerLookaheadSmoothingVelocityConstant, throttleLeverManager);
            this.torqueSimVar = `TURB ENG MAX TORQUE PERCENT:${this.index}`;
        }
        /** @inheritdoc */
        getPower() {
            return SimVar.GetSimVarValue(this.torqueSimVar, exports.SimVarValueType.Percent);
        }
    }

    /**
     * Transports an alert state to the CAS via the event bus.
     */
    class CasAlertTransporter {
        /**
         * Creates an instance of a CasAlertTransporter.
         * @param bus The event bus to use with this instance.
         * @param uuid The alert UUID.
         * @param priority The alert priority.
         * @param suffix The alert suffix.
         */
        constructor(bus, uuid, priority, suffix) {
            this.bus = bus;
            this.uuid = uuid;
            this.priority = priority;
            this.suffix = suffix;
            this.currentValue = false;
            this.subs = [];
            this.updateEntries = [];
            this.isAlive = true;
            this.isPaused = false;
        }
        /**
         * Sets whether or not the alert is active.
         * @param active Whether or not the alert is active.
         * @throws Error if this transporter has been destroyed.
         */
        set(active) {
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot change an alert with a dead transporter');
            }
            if (this.currentValue !== active) {
                if (active) {
                    this.bus.getPublisher().pub('cas_activate_alert', { key: { uuid: this.uuid, suffix: this.suffix }, priority: this.priority }, true, false);
                }
                else {
                    this.bus.getPublisher().pub('cas_deactivate_alert', { key: { uuid: this.uuid, suffix: this.suffix }, priority: this.priority }, true, false);
                }
                this.currentValue = active;
            }
        }
        /**
         * Binds an alert state to a subscribable value.
         * @param toWatch The subscribable value to watch.
         * @param predicate The predicate that transforms the value into a boolean alert activity state.
         * @returns The modified alert transporter.
         * @throws Error if this transporter has been destroyed.
         */
        bind(toWatch, predicate) {
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot bind an alert state using a dead transporter');
            }
            this.subs.push(toWatch.sub(v => this.set(predicate(v)), true, this.isPaused));
            return this;
        }
        /**
         * Binds the alert state to an update loop.
         * @param predicate The predicate that transforms the value into a boolean alert activity state.
         * @returns The modified alert transporter.
         * @throws Error if this transporter has been destroyed.
         */
        bindUpdate(predicate) {
            var _a;
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot bind an alert state using a dead transporter');
            }
            const entry = {
                isPaused: this.isPaused,
                hasState: false,
                func: (deltaTime) => this.set(predicate(deltaTime))
            };
            this.updateEntries.push(entry);
            this.initUpdateFuncs();
            (_a = CasAlertTransporter.updateEntries) === null || _a === void 0 ? void 0 : _a.push(entry);
            return this;
        }
        /**
         * Binds the alert state to an update loop.
         * @param predicate The predicate that transforms the value into a boolean alert activity state.
         * @param state The optional state to pass into the predicate.
         * @returns The modified alert transporter.
         * @throws Error if this transporter has been destroyed.
         */
        bindStateUpdate(predicate, state) {
            var _a;
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot bind an alert state using a dead transporter');
            }
            const entry = {
                isPaused: this.isPaused,
                hasState: true,
                func: (deltaTime, stateInner) => this.set(predicate(deltaTime, stateInner)),
                state
            };
            this.updateEntries.push(entry);
            this.initUpdateFuncs();
            (_a = CasAlertTransporter.updateEntries) === null || _a === void 0 ? void 0 : _a.push(entry);
            return this;
        }
        /**
         * Resumes this transporter. When this transporter is paused, any subscribables or update loops used to bind the state
         * of this transporter's alert are also resumed. On resumption, the values of bound subscribables are evaluated
         * immediately, while the values of bound update loops will be evaluated during the next update cycle.
         * @returns This transporter, after it has been resumed.
         * @throws Error if this transporter has been destroyed.
         */
        resume() {
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot resume a dead transporter');
            }
            if (!this.isPaused) {
                return this;
            }
            this.isPaused = false;
            this.subs.forEach(sub => { sub.resume(true); });
            this.updateEntries.forEach(entry => { entry.isPaused = false; });
            return this;
        }
        /**
         * Pauses this transporter. When this transporter is paused, any subscribables or update loops used to bind the state
         * of this transporter's alert are also paused.
         * @returns This transporter, after it has been paused.
         * @throws Error if this transporter has been destroyed.
         */
        pause() {
            if (!this.isAlive) {
                throw new Error('CasAlertTransporter: cannot pause a dead transporter');
            }
            if (this.isPaused) {
                return this;
            }
            this.isPaused = true;
            this.subs.forEach(sub => { sub.pause(); });
            if (CasAlertTransporter.updateEntries) {
                this.updateEntries.forEach(entry => {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    const index = CasAlertTransporter.updateEntries.indexOf(entry);
                    if (index >= 0) {
                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                        CasAlertTransporter.updateEntries.splice(index, 1);
                    }
                });
            }
            return this;
        }
        /**
         * Destroys this transporter. This will destroy any subscribables or update loops used to bind the state of this
         * transporter's alert. Once this transporter has been destroyed, it cannot be used to change the state of its alert,
         * bind the state of its alert, or be paused or resumed.
         */
        destroy() {
            this.isAlive = false;
            this.subs.forEach(sub => { sub.destroy(); });
            this.updateEntries.forEach(entry => { entry.isPaused = true; });
        }
        /**
         * Creates an instance of an AlertTransporter.
         * @param bus The event bus to use with this instance.
         * @param uuid The alert UUID.
         * @param priority The alert priority.
         * @param suffix The alert suffix.
         * @returns The created AlertTransporter.
         */
        static create(bus, uuid, priority, suffix) {
            return new CasAlertTransporter(bus, uuid, priority, suffix);
        }
        /**
         * Initializes the update functions.
         */
        initUpdateFuncs() {
            if (CasAlertTransporter.updateEntries === undefined) {
                CasAlertTransporter.updateEntries = [];
                this.bus.getSubscriber().on('simTime').handle((timestamp) => {
                    if (CasAlertTransporter.previousTimestamp === -1) {
                        CasAlertTransporter.previousTimestamp = timestamp;
                    }
                    const deltaTime = MathUtils.clamp(timestamp - CasAlertTransporter.previousTimestamp, 0, 10000);
                    if (CasAlertTransporter.updateEntries !== undefined) {
                        for (let i = 0; i < CasAlertTransporter.updateEntries.length; i++) {
                            const entry = CasAlertTransporter.updateEntries[i];
                            if (entry.isPaused) {
                                continue;
                            }
                            if (entry.hasState) {
                                entry.func(deltaTime, entry.state);
                            }
                            else {
                                entry.func(deltaTime);
                            }
                        }
                    }
                    CasAlertTransporter.previousTimestamp = timestamp;
                });
            }
        }
    }
    CasAlertTransporter.previousTimestamp = -1;

    /**
     * This is a convenience tool for publishing and republishing CAS alert registrations.  Code
     * that wants to implement its own alert publishing system can use the CasRegistrationManager
     * to avoid having to subscribe to and handle requests on the bus for republication of alert
     * registrations.
     */
    class CasRegistrationManager {
        /**
         * Create a CasRegistrationManager
         * @param bus The event bus
         */
        constructor(bus) {
            this.registrations = new Map();
            this.publisher = bus.getPublisher();
            const subscriber = bus.getSubscriber();
            subscriber.on('cas_publish_registration').handle(uuid => this.publishRegistration(uuid));
            subscriber.on('cas_publish_all_registrations').handle(() => this.publishAllRegistrations());
        }
        /**
         * Register an alert for management.
         * @param definition The CasAlertDefinition for this alert.
         */
        register(definition) {
            this.registrations.set(definition.uuid, definition);
            this.publishRegistration(definition.uuid);
        }
        /**
         * Handle publishing information for a single alert.
         * @param uuid The UUID of the registation to publish.
         */
        publishRegistration(uuid) {
            const definition = this.registrations.get(uuid);
            if (definition) {
                this.publisher.pub('cas_register_alert', definition, true, false);
            }
        }
        /**
         * Publish all current registrations.
         */
        publishAllRegistrations() {
            for (const uuid of this.registrations.keys()) {
                this.publishRegistration(uuid);
            }
        }
    }

    /**
     * A bus-based system for publishing and managing CAS notifications.
     */
    /**
     * A system for CAS management.
     *
     * Every avionics system must have exactly one instance of CasSystem configured as the primary system. This is the one
     * that is responsible for triggering sim-level master caution/warning alerts and intercepting and handling master
     * acknowledge events.
     *
     * Each JS instrument should have at most one instance of CasSystem. Multiple instances of CasSystem on a single
     * instrument will cause duplicated events to be published to the topics defined by {@link CasStateEvents}.
     */
    class CasSystem {
        /**
         * Create a CasSystem.
         * @param bus The event bus.
         * @param primary Whether or not this is the system responsible for managing alerts at the sim level.
         */
        constructor(bus, primary = false) {
            this.registeredAlerts = new Map();
            this.activeInhibitStates = new Set();
            this.scheduledSuffixedAlerts = new Map();
            this.scheduledUnsuffixedAlerts = new Map();
            this.previousScheduleCheckTime = -1;
            this.initialAcknowledge = true;
            this.masterWarningActive = undefined;
            this.masterCautionActive = undefined;
            /** A compound map containing every active CAS message at every message priority. */
            this.allMessages = new Map([
                [exports.AnnunciationType.Warning, new Map()],
                [exports.AnnunciationType.Caution, new Map()],
                [exports.AnnunciationType.Advisory, new Map()],
                [exports.AnnunciationType.SafeOp, new Map()],
            ]);
            /** An array of CAS messages filtered to only show the highest priority for any given UUID and suffix. */
            this.displayedCasMessages = ArraySubject.create();
            this.prevDisplayedCasMessages = [];
            /** The displayable CAS messages sorted by the standard sort order of priority, state, and age. */
            // TODO Add the ability to specify alternate sorting logic.
            this._casActiveMessageSubject = SortedMappedSubscribableArray.create(this.displayedCasMessages, (a, b) => {
                if (a.uuid && b.uuid && a.lastActive !== undefined && b.lastActive !== undefined) {
                    if (a.priority === b.priority) {
                        return b.lastActive - a.lastActive;
                    }
                    return a.priority - b.priority;
                }
                else {
                    return 0;
                }
            }, (a, b) => a.uuid === b.uuid);
            this.casActiveMessageSubject = this._casActiveMessageSubject;
            this.bus = bus;
            this.casSubscriber = this.bus.getSubscriber();
            this.casPublisher = this.bus.getPublisher();
            this.casStatePublisher = this.bus.getPublisher();
            this.isPrimary = primary;
            this.setMasterStatus(exports.AnnunciationType.Caution, false);
            this.setMasterStatus(exports.AnnunciationType.Warning, false);
            this.bus.getSubscriber().on('simTime').handle(t => this.checkScheduledAlerts(t));
            // If this is the primary CAS system, set up handlers for the master caution and warning
            // acknowledgement events and reset the initial alert state.
            if (this.isPrimary) {
                KeyEventManager.getManager(this.bus).then(manager => {
                    manager.interceptKey('MASTER_CAUTION_ACKNOWLEDGE', true);
                    manager.interceptKey('MASTER_WARNING_ACKNOWLEDGE', true);
                });
                this.bus.getSubscriber().on('key_intercept').handle((keyData) => {
                    switch (keyData.key) {
                        case 'MASTER_CAUTION_ACKNOWLEDGE':
                            this.casPublisher.pub('cas_master_acknowledge', exports.AnnunciationType.Caution, true, false);
                            break;
                        case 'MASTER_WARNING_ACKNOWLEDGE':
                            this.casPublisher.pub('cas_master_acknowledge', exports.AnnunciationType.Warning, true, false);
                            break;
                    }
                });
            }
            // Set up subs to the events we react to.
            this.casSubscriber.on('cas_register_alert').handle(alertConfig => {
                if (!this.registeredAlerts.has(alertConfig.uuid)) {
                    this.registeredAlerts.set(alertConfig.uuid, alertConfig);
                }
            });
            this.casSubscriber.on('cas_activate_alert').handle(eventData => {
                this.scheduleAlert(eventData.key, eventData.priority);
            });
            this.casSubscriber.on('cas_deactivate_alert').handle(eventData => {
                this.deactivateAlert(eventData.key, eventData.priority);
            });
            this.casSubscriber.on('cas_activate_inhibit_state').handle(state => {
                this.handleInhibitState(state, true);
            });
            this.casSubscriber.on('cas_deactivate_inhibit_state').handle(state => {
                this.handleInhibitState(state, false);
            });
            this.casSubscriber.on('cas_set_initial_acknowledge').handle(v => {
                this.initialAcknowledge = v;
            });
            this.casSubscriber.on('cas_master_acknowledge').handle(ackType => {
                this.handleAcknowledgement(ackType);
            });
            this.casSubscriber.on('cas_single_acknowledge').handle(({ key, priority }) => {
                this.handleSingleAcknowledgement(key, priority);
            });
            // Requests the CAS system to suppress all annunciations with the provided priority.
            this.casSubscriber.on('cas_suppress_priority').handle(priority => {
                this.suppressType(priority);
            });
            // Requests the CAS system to unsuppress all annunciations with the provided priority.
            this.casSubscriber.on('cas_unsuppress_priority').handle(priority => {
                this.unsuppressAllSuppressed(priority);
            });
            this.casPublisher.pub('cas_publish_all_registrations', true, true);
        }
        /**
         * Suppress all messages of a given type.
         * @param priority The type of messages which shall be suppressed.
         */
        suppressType(priority) {
            for (const [, message] of this.allMessages.get(priority) || []) {
                message.suppressed = true;
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Unsuppress all messages of a given type.
         * @param priority The type of messages which shall be suppressed.
         */
        unsuppressAllSuppressed(priority) {
            for (const [, message] of this.allMessages.get(priority) || []) {
                message.suppressed = false;
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Create a new message from an alert key at a given priority.
         * @param alertKey The alert key.
         * @param priority The priority.
         * @returns A new CasActiveMessage or undefined if the key was invalid.
         */
        createNewMessage(alertKey, priority) {
            var _a;
            const def = this.registeredAlerts.get(alertKey.uuid);
            if (def === undefined) {
                return undefined;
            }
            let inhibited = false;
            for (const state of def.inhibitedBy || []) {
                if (this.activeInhibitStates.has(state)) {
                    inhibited = true;
                    break;
                }
            }
            return {
                uuid: alertKey.uuid,
                message: (_a = def.message) !== null && _a !== void 0 ? _a : 'MISSING MESSAGE',
                priority: priority,
                acknowledged: this.initialAcknowledge,
                inhibited: inhibited,
                suppressed: false,
                lastActive: Date.now(),
                suffixes: alertKey.suffix !== undefined ? [alertKey.suffix] : undefined,
                acknowledgedSuffixes: alertKey.suffix !== undefined ? this.initialAcknowledge ? [alertKey.suffix] : [] : undefined
            };
        }
        /**
         * Schedule an alert to go active at the end of its debounce time.
         * @param alertKey The UUID and optional suffix of the alert to handle.
         * @param priority The priority of the alert to fire.
         */
        scheduleAlert(alertKey, priority) {
            var _a;
            if (!this.checkValidAlertKey(alertKey)) {
                return;
            }
            const debounceTime = (_a = this.registeredAlerts.get(alertKey.uuid)) === null || _a === void 0 ? void 0 : _a.debounceTime;
            if (debounceTime === undefined) {
                this.activateAlert(alertKey, priority);
                return;
            }
            // Traverse the nested maps of scheduled alerts and create a schedule item if needed.
            if (alertKey.suffix !== undefined) {
                let uuidMap = this.scheduledSuffixedAlerts.get(alertKey.uuid);
                if (uuidMap === undefined) {
                    uuidMap = new Map();
                    this.scheduledSuffixedAlerts.set(alertKey.uuid, uuidMap);
                }
                let suffixMap = uuidMap.get(alertKey.suffix);
                if (suffixMap === undefined) {
                    suffixMap = new Map();
                    uuidMap.set(alertKey.suffix, suffixMap);
                }
                const time = suffixMap.get(priority);
                if (time !== undefined) {
                    return;
                }
                suffixMap.set(priority, debounceTime);
            }
            else {
                let uuidMap = this.scheduledUnsuffixedAlerts.get(alertKey.uuid);
                if (uuidMap === undefined) {
                    uuidMap = new Map();
                    this.scheduledUnsuffixedAlerts.set(alertKey.uuid, uuidMap);
                }
                const time = uuidMap.get(priority);
                if (time !== undefined) {
                    return;
                }
                uuidMap.set(priority, debounceTime);
            }
        }
        /**
         * Check the scheduled alerts to see if there's anything that needs to fire.
         * @param timestamp The current sim time.
         */
        checkScheduledAlerts(timestamp) {
            if (this.previousScheduleCheckTime !== -1) {
                const deltaTime = NavMath.clamp(timestamp - this.previousScheduleCheckTime, 0, 10000);
                if (deltaTime > 0) {
                    // Handle unsuffixed alerts.
                    for (const [uuid, uuidMap] of this.scheduledUnsuffixedAlerts) {
                        for (const [priority, delay] of uuidMap) {
                            const newDelay = delay - deltaTime;
                            if (newDelay <= 0) {
                                uuidMap.delete(priority);
                                this.activateAlert({ uuid: uuid }, priority);
                            }
                            else {
                                uuidMap.set(priority, newDelay);
                            }
                        }
                    }
                    // And then suffixed ones.
                    for (const [uuid, uuidMap] of this.scheduledSuffixedAlerts) {
                        // eslint-disable-next-line @typescript-eslint/no-unused-vars
                        for (const [suffix, suffixMap] of uuidMap) {
                            for (const [priority, delay] of suffixMap) {
                                const newDelay = delay - deltaTime;
                                if (newDelay <= 0) {
                                    suffixMap.delete(priority);
                                    this.activateAlert({ uuid: uuid, suffix: suffix }, priority);
                                }
                                else {
                                    suffixMap.set(priority, newDelay);
                                }
                            }
                        }
                    }
                }
            }
            this.previousScheduleCheckTime = timestamp;
        }
        /**
         * Handle an alert going active.
         * @param alertKey The UUID and optional suffix of the alert to handle.
         * @param priority The priority of the alert to fire.
         */
        activateAlert(alertKey, priority) {
            var _a;
            if (!this.checkValidAlertKey(alertKey)) {
                return;
            }
            // Check to see if there's an existing alert for this message at this priority level.
            const messagesAtPriority = this.allMessages.get(priority);
            const uuidMessageAtPriority = messagesAtPriority === null || messagesAtPriority === void 0 ? void 0 : messagesAtPriority.get(alertKey.uuid);
            if (uuidMessageAtPriority === undefined) {
                // There's not already an existing message, so we make one.
                const newMessage = this.createNewMessage(alertKey, priority);
                if (newMessage !== undefined) {
                    messagesAtPriority === null || messagesAtPriority === void 0 ? void 0 : messagesAtPriority.set(alertKey.uuid, newMessage);
                }
            }
            else {
                // There is an existing message at this priority level so we need to update it instead.
                uuidMessageAtPriority.acknowledged && (uuidMessageAtPriority.acknowledged = this.initialAcknowledge);
                uuidMessageAtPriority.lastActive = Date.now();
                // Suffix handling.  If one is in the alert key, make sure it's added to the active
                // message if it's not already present.
                if (alertKey.suffix !== undefined) {
                    // These arrays must be defined or else checkValidAlertKey() would have returned false.
                    const suffixes = uuidMessageAtPriority.suffixes;
                    const acknowledgedSuffixes = uuidMessageAtPriority.acknowledgedSuffixes;
                    if (!suffixes.includes(alertKey.suffix)) {
                        suffixes.push(alertKey.suffix);
                        if (this.initialAcknowledge) {
                            acknowledgedSuffixes.push(alertKey.suffix);
                        }
                        const suffixOrder = (_a = this.registeredAlerts.get(alertKey.uuid)) === null || _a === void 0 ? void 0 : _a.suffixes;
                        if (suffixOrder !== undefined) {
                            const comparator = (a, b) => suffixOrder.indexOf(a) - suffixOrder.indexOf(b);
                            suffixes.sort(comparator);
                            if (this.initialAcknowledge) {
                                acknowledgedSuffixes.sort(comparator);
                            }
                        }
                    }
                }
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Handle an alert going inactive.
         * @param alertKey The UUID and optional suffix of the alert to handle.
         * @param priority The priority of the alert to clear.
         */
        deactivateAlert(alertKey, priority) {
            var _a, _b, _c, _d;
            if (!this.checkValidAlertKey(alertKey)) {
                return;
            }
            // We are deactivating an alert.  If there is no suffix provided this is easy.
            if (alertKey.suffix === undefined) {
                (_a = this.allMessages.get(priority)) === null || _a === void 0 ? void 0 : _a.delete(alertKey.uuid);
                (_b = this.scheduledUnsuffixedAlerts.get(alertKey.uuid)) === null || _b === void 0 ? void 0 : _b.delete(priority);
            }
            else {
                (_d = (_c = this.scheduledSuffixedAlerts.get(alertKey.uuid)) === null || _c === void 0 ? void 0 : _c.get(alertKey.suffix)) === null || _d === void 0 ? void 0 : _d.delete(priority);
                // With suffixes in the mix we need a little more intelligence.  First, find the relevant message.
                const messagesAtPriority = this.allMessages.get(priority);
                const uuidMessageAtPriority = messagesAtPriority === null || messagesAtPriority === void 0 ? void 0 : messagesAtPriority.get(alertKey.uuid);
                if (uuidMessageAtPriority !== undefined) {
                    // These arrays must be defined or else checkValidAlertKey() would have returned false.
                    const suffixes = uuidMessageAtPriority.suffixes;
                    const acknowledgedSuffixes = uuidMessageAtPriority.acknowledgedSuffixes;
                    // Remove the suffix from the message's suffix array.
                    const index = suffixes.indexOf(alertKey.suffix);
                    if (index >= 0) {
                        suffixes.splice(index, 1);
                    }
                    if (suffixes.length == 0) {
                        // We've just removed the last suffix, we can fully disable this alert.
                        messagesAtPriority === null || messagesAtPriority === void 0 ? void 0 : messagesAtPriority.delete(alertKey.uuid);
                    }
                    else {
                        // Remove the suffix from the message's acknowledged suffix array.
                        const acknowledgedIndex = acknowledgedSuffixes.indexOf(alertKey.suffix);
                        if (acknowledgedIndex >= 0) {
                            acknowledgedSuffixes.splice(acknowledgedIndex, 1);
                        }
                    }
                }
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Handle a master warning or caution acknowledgement.
         * @param type The type of alert to acknowledge.
         */
        handleAcknowledgement(type) {
            this.setMasterStatus(type, false);
            const messagesAtPriority = this.allMessages.get(type);
            if (messagesAtPriority !== undefined) {
                for (const uuid of messagesAtPriority.keys()) {
                    const message = messagesAtPriority.get(uuid);
                    if (message !== undefined && !message.inhibited) {
                        this.acknowledgeMessage(message);
                    }
                }
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Handle acknowledgement of a single message
         * @param key The UUID and optional suffix of the alert to handle.
         * @param priority The priority of the alert to handle.
         */
        handleSingleAcknowledgement(key, priority) {
            const messagesAtPriority = this.allMessages.get(priority);
            if (messagesAtPriority) {
                const messageToBeAck = messagesAtPriority.get(key.uuid);
                if (messageToBeAck !== undefined && !messageToBeAck.inhibited) {
                    this.acknowledgeMessage(messageToBeAck);
                    this.refreshDisplayedAlerts();
                }
            }
        }
        /**
         * Acknowledge a single message by mutating it.
         * @param message The message to be acknowledged from CasSystem's `allMessages`
         */
        acknowledgeMessage(message) {
            message.acknowledged = true;
            if (message.suffixes && message.acknowledgedSuffixes) {
                // Copy the suffixes array into the acknowledged suffixes array since every suffix is now acknowledged
                message.acknowledgedSuffixes.length = message.suffixes.length;
                for (let i = 0; i < message.suffixes.length; i++) {
                    const suffix = message.suffixes[i];
                    if (message.acknowledgedSuffixes[i] !== suffix) {
                        message.acknowledgedSuffixes.splice(i, 0, suffix);
                        this.casStatePublisher.pub('cas_alert_acknowledged', {
                            uuid: message.uuid,
                            suffix: message.suffixes[i],
                            priority: message.priority,
                            acknowledged: true
                        }, false, false);
                    }
                }
            }
            else {
                this.casStatePublisher.pub('cas_alert_acknowledged', {
                    uuid: message.uuid,
                    priority: message.priority,
                    acknowledged: true
                }, false, false);
            }
        }
        /**
         * Handle the setting of a new inhibit state.
         * @param state The name of the inhibited state to set.
         * @param active Whether the state is active or not.
         */
        handleInhibitState(state, active) {
            var _a, _b, _c, _d;
            if ((active && !this.activeInhibitStates.has(state)) || (!active && this.activeInhibitStates.has(state))) {
                if (active) {
                    this.activeInhibitStates.add(state);
                }
                else {
                    this.activeInhibitStates.delete(state);
                }
                for (const priority of this.allMessages.keys()) {
                    for (const message of (_b = (_a = this.allMessages.get(priority)) === null || _a === void 0 ? void 0 : _a.values()) !== null && _b !== void 0 ? _b : []) {
                        if (message !== undefined) {
                            let isInhibited = false;
                            for (const candidateState of (_d = (_c = this.registeredAlerts.get(message.uuid)) === null || _c === void 0 ? void 0 : _c.inhibitedBy) !== null && _d !== void 0 ? _d : []) {
                                if (this.activeInhibitStates.has(candidateState)) {
                                    isInhibited = true;
                                    break;
                                }
                            }
                            message.inhibited = isInhibited;
                        }
                    }
                }
            }
            this.refreshDisplayedAlerts();
        }
        /**
         * Set both sets of simvars relevant to a master caution or warning status.
         * @param type The type of the status to set
         * @param active Whether or not the status is active
         */
        setMasterStatus(type, active) {
            switch (type) {
                case exports.AnnunciationType.Caution:
                    if (this.masterCautionActive !== active) {
                        if (this.isPrimary) {
                            SimVar.SetSimVarValue('K:MASTER_CAUTION_SET', exports.SimVarValueType.Number, active ? 1 : 0);
                            SimVar.SetSimVarValue('L:Generic_Master_Caution_Active', exports.SimVarValueType.Bool, active);
                        }
                        this.masterCautionActive = active;
                        this.casStatePublisher.pub('cas_master_caution_active', active, false, true);
                    }
                    break;
                case exports.AnnunciationType.Warning:
                    if (this.masterWarningActive !== active) {
                        if (this.isPrimary) {
                            SimVar.SetSimVarValue('K:MASTER_WARNING_SET', exports.SimVarValueType.Number, active ? 1 : 0);
                            SimVar.SetSimVarValue('L:Generic_Master_Warning_Active', exports.SimVarValueType.Bool, active);
                        }
                        this.masterWarningActive = active;
                        this.casStatePublisher.pub('cas_master_warning_active', active, false, true);
                    }
                    break;
            }
        }
        /**
         * Get the full registered definition of an alert based on its key.
         * @param alertKey The key of the alert definition to retrieve.
         * @returns The full alert definition, or undefined if not found.
         */
        getAlertDefinition(alertKey) {
            const alertDefinition = this.registeredAlerts.get(alertKey.uuid);
            if (alertDefinition === undefined) {
                console.warn(`Trying to access an unregistered alert UUID: ${alertKey.uuid}`);
                return undefined;
            }
            else {
                return alertDefinition;
            }
        }
        /**
         * Check whether a provided alert key is valid according to the alert's suffix definition.
         * @param alertKey The key of the alert to check.
         * @returns A boolean indicating whether the key is valid.
         */
        checkValidAlertKey(alertKey) {
            const alertDefinition = this.getAlertDefinition(alertKey);
            if (alertDefinition === undefined) {
                return false;
            }
            const suffixes = alertDefinition.suffixes;
            if (alertKey.suffix === undefined && suffixes !== undefined) {
                console.warn(`Trying to access a suffixed alert without a suffix: '${alertDefinition.message}'`);
                return false;
            }
            else if (alertKey.suffix !== undefined && suffixes === undefined) {
                console.warn(`Trying to access a non-suffixed alert with a suffix: '${alertDefinition.message}:${alertKey.suffix}'`);
                return false;
            }
            else if (alertKey.suffix !== undefined && !(suffixes === null || suffixes === void 0 ? void 0 : suffixes.includes(alertKey.suffix))) {
                console.warn(`Trying to access a suffixed alert with an invalid suffix: '${alertDefinition.message}:${alertKey.suffix}'`);
                return false;
            }
            return true;
        }
        /**
         * See if there is still an active, unacked annunciation of the given type.
         * @param type The annunciation type to check for.
         * @returns True if there is an active, unacked annunciation of the given type, false otherwise.
         */
        checkForActiveType(type) {
            const messagesAtPriority = this.allMessages.get(type);
            if (messagesAtPriority !== undefined) {
                for (const message of messagesAtPriority.values()) {
                    if (!message.acknowledged) {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * Reprocess all active alerts to generate a list consisting only of those which are displayable.
         * At the same time, keep track of whether or not the master warning or caution lights should be
         * active and, if the primary CasSystem, set them appropriately when done.
         */
        refreshDisplayedAlerts() {
            this.displayedCasMessages.clear();
            const unsuffixedDisplayedKeys = new Set();
            const suffixedDisplayedKeys = new Map();
            let unackedWarnings = false;
            let unackedCautions = false;
            for (const priority of [
                exports.AnnunciationType.Warning,
                exports.AnnunciationType.Caution,
                exports.AnnunciationType.Advisory,
                exports.AnnunciationType.SafeOp
            ]) {
                // Go through the UUIDs of all active messages at this priority.
                for (const [uuid, message] of this.allMessages.get(priority) || []) {
                    // If the message is currently inhibited or suppressed, skip over it.
                    if (message.inhibited || message.suppressed) {
                        continue;
                    }
                    // In the simple case we have no suffixes.   In which case we either add
                    // the message to the displayed array or continue.
                    if (message.suffixes === undefined) {
                        if (!unsuffixedDisplayedKeys.has(uuid)) {
                            unsuffixedDisplayedKeys.add(uuid);
                            this.displayedCasMessages.insert(message);
                            if (!message.acknowledged) {
                                switch (priority) {
                                    case exports.AnnunciationType.Warning:
                                        unackedWarnings = true;
                                        break;
                                    case exports.AnnunciationType.Caution:
                                        unackedCautions = true;
                                        break;
                                }
                            }
                        }
                    }
                    else {
                        // We do have suffixes to worry about.  Yay.  Let's store all the ones
                        // that are supposed to be displayed at this level and reset the list
                        // in our message.
                        const origSuffixes = message.suffixes;
                        message.suffixes = [];
                        // Now we go through the active suffixes and see if they've already been
                        // displayed at a higher priority.
                        for (const suffix of origSuffixes) {
                            const suffixesDisplayed = suffixedDisplayedKeys.get(uuid);
                            if (suffixesDisplayed === undefined) {
                                // First time we've seen any suffix for this UUID
                                suffixedDisplayedKeys.set(uuid, new Set([suffix]));
                                message.suffixes.push(suffix);
                            }
                            else {
                                // We've already displayed some suffix for this UID.  But is is the one
                                // we're working with now?
                                if (!suffixesDisplayed.has(suffix)) {
                                    suffixesDisplayed.add(suffix);
                                    message.suffixes.push(suffix);
                                }
                            }
                        }
                        // Now, assuming we added back at least one suffix we want to display this
                        // message, so add it to the array.
                        if (message.suffixes.length > 0) {
                            this.displayedCasMessages.insert(message);
                            if (!message.acknowledged) {
                                switch (priority) {
                                    case exports.AnnunciationType.Warning:
                                        unackedWarnings = true;
                                        break;
                                    case exports.AnnunciationType.Caution:
                                        unackedCautions = true;
                                        break;
                                }
                            }
                        }
                    }
                }
            }
            this.setMasterStatus(exports.AnnunciationType.Caution, unackedCautions);
            this.setMasterStatus(exports.AnnunciationType.Warning, unackedWarnings);
            // Diff the old and new messages and publish state change events.
            const oldMessages = this.prevDisplayedCasMessages;
            const newMessages = this.displayedCasMessages.getArray();
            this.diffAlerts(newMessages, oldMessages, 'cas_alert_hidden');
            this.diffAlerts(oldMessages, newMessages, 'cas_alert_displayed');
            // We need to deep copy the new displayed messages array because messages in the displayed array can be mutated.
            CasSystem.copyMessageArray(newMessages, this.prevDisplayedCasMessages);
        }
        /**
         * Finds alerts displayed in a query message array that are not displayed in a reference message array and publishes
         * them.
         * @param referenceMessages The reference message array.
         * @param queryMessages The query message array.
         * @param topic The event bus topic to publish to.
         */
        diffAlerts(referenceMessages, queryMessages, topic) {
            // Using old-fashioned for loops here to avoid object/function creation
            for (let i = 0; i < queryMessages.length; i++) {
                const queryMessage = queryMessages[i];
                let matchedReferenceMessage = undefined;
                for (let j = 0; j < referenceMessages.length; j++) {
                    const referenceMessage = referenceMessages[j];
                    if (referenceMessage.priority === queryMessage.priority && referenceMessage.uuid === queryMessage.uuid) {
                        matchedReferenceMessage = referenceMessage;
                        break;
                    }
                }
                // If the new message has suffixes, then we need to check each suffix to see if a matched message contained the
                // same suffix. Otherwise, we need to check if a matched message exists and had no suffix.
                if (queryMessage.suffixes && queryMessage.suffixes.length > 0) {
                    for (let k = 0; k < queryMessage.suffixes.length; k++) {
                        const suffix = queryMessage.suffixes[k];
                        if (!matchedReferenceMessage || !matchedReferenceMessage.suffixes || !matchedReferenceMessage.suffixes.includes(suffix)) {
                            this.casStatePublisher.pub(topic, {
                                uuid: queryMessage.uuid,
                                suffix,
                                priority: queryMessage.priority,
                                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                                acknowledged: queryMessage.acknowledgedSuffixes.includes(suffix)
                            }, false, false);
                        }
                    }
                }
                else if (!matchedReferenceMessage || (matchedReferenceMessage.suffixes && matchedReferenceMessage.suffixes.length > 0)) {
                    this.casStatePublisher.pub(topic, {
                        uuid: queryMessage.uuid,
                        priority: queryMessage.priority,
                        acknowledged: queryMessage.acknowledged
                    }, false, false);
                }
            }
        }
        /**
         * Copies a CAS message.
         * @param source The source message to copy from.
         * @param target The target message to copy to. If not defined, a new message object will be created.
         * @returns The message copy.
         */
        static copyMessage(source, target) {
            target !== null && target !== void 0 ? target : (target = {});
            target.uuid = source.uuid;
            target.message = source.message;
            target.priority = source.priority;
            target.acknowledged = source.acknowledged;
            target.inhibited = source.inhibited;
            target.suppressed = source.suppressed;
            target.lastActive = source.lastActive;
            if (source.suffixes) {
                target.suffixes = ArrayUtils.shallowCopy(source.suffixes, target.suffixes);
            }
            else {
                target.suffixes = undefined;
            }
            if (source.acknowledgedSuffixes) {
                target.acknowledgedSuffixes = ArrayUtils.shallowCopy(source.acknowledgedSuffixes, target.acknowledgedSuffixes);
            }
            else {
                target.acknowledgedSuffixes = undefined;
            }
            return target;
        }
        /**
         * Performs a deep copy of a CAS message array.
         * @param source The source array to copy from.
         * @param target The target array to copy to. If not defined, a new array will be created.
         * @returns The array copy.
         */
        static copyMessageArray(source, target) {
            target !== null && target !== void 0 ? target : (target = []);
            target.length = source.length;
            for (let i = 0; i < source.length; i++) {
                target[i] = CasSystem.copyMessage(source[i], target[i]);
            }
            return target;
        }
    }

    /**
     * An adapter to convert legacy panel.xml annunciations into the new CasSystem.
     */
    class CasSystemLegacyAdapter {
        /**
         * Create a CasSystemLegacyAdapter
         * @param bus The event bus.
         * @param logicHost A CompositeLogicXMLHost for running the events.
         * @param annunciationDefs An array of system annunciations to monitor.
         */
        constructor(bus, logicHost, annunciationDefs) {
            var _a;
            this.logicHost = logicHost;
            /** A map of logic elements for non-suffixed events. */
            this.nonSuffixLogic = new Map();
            /** A map of logic elements for suffixed events, mapped by suffix. */
            this.suffixLogic = new Map();
            /** Alert definitions and priorities based on the legacy configuration for feeding into the new system. */
            this.annunciations = new Map();
            this.regManager = new CasRegistrationManager(bus);
            this.pub = bus.getPublisher();
            // Go through the legacy annunciation definitions and create a bunch of definitions suitable
            // for the new CAS system, each tied to an XML logic element for managind the alert state.
            for (let i = 0; i < annunciationDefs.length; i++) {
                const legacy = annunciationDefs[i];
                const annunciation = this.annunciations.get(legacy.uuid);
                if (annunciation === undefined) {
                    // No annunciation with this UUID has been encountered, so create a new one.
                    this.annunciations.set(legacy.uuid, {
                        def: {
                            uuid: legacy.uuid,
                            message: legacy.text,
                            suffixes: legacy.suffix !== undefined ? [legacy.suffix] : undefined
                        },
                        priority: legacy.type
                    });
                    if (legacy.suffix === undefined) {
                        this.nonSuffixLogic.set(legacy.uuid, legacy.condition);
                    }
                    else {
                        this.suffixLogic.set(legacy.uuid, new Map([[legacy.suffix, legacy.condition]]));
                    }
                }
                else {
                    // We've already seen the UUID for this message, so we need to do some suffix management.
                    if (legacy.suffix !== undefined) {
                        (_a = annunciation.def.suffixes) === null || _a === void 0 ? void 0 : _a.push(legacy.suffix);
                        const suffixLogicMap = this.suffixLogic.get(legacy.uuid);
                        if (suffixLogicMap === undefined) {
                            console.warn(`Trying to add logic to a UUID that doesn't exist: ${legacy.uuid}`);
                        }
                        else {
                            suffixLogicMap.set(legacy.suffix, legacy.condition);
                        }
                    }
                }
            }
        }
        /** Register all the alerts and begin monitoring. */
        start() {
            for (const [uuid, info] of this.annunciations) {
                this.regManager.register({
                    uuid: uuid,
                    message: info.def.message,
                    suffixes: info.def.suffixes
                });
                if (info.def.suffixes === undefined) {
                    const logic = this.nonSuffixLogic.get(uuid);
                    if (logic === undefined) {
                        console.warn(`Logic missing for non-suffixed UUID ${uuid}`);
                    }
                    else {
                        this.logicHost.addLogicAsNumber(logic, (v) => {
                            if (v == 1) {
                                this.pub.pub('cas_activate_alert', { key: { uuid: uuid }, priority: info.priority }, true, false);
                            }
                            else {
                                this.pub.pub('cas_deactivate_alert', { key: { uuid: uuid }, priority: info.priority }, true, false);
                            }
                        }, 0);
                    }
                }
                else {
                    // We have something with suffixes, so we have to deal with that with some extra logic.
                    const suffixMap = this.suffixLogic.get(uuid);
                    if (suffixMap === undefined) {
                        console.warn(`Cannot find suffix logic for UUID ${uuid}`);
                    }
                    else {
                        for (const suffix of info.def.suffixes) {
                            const logic = suffixMap.get(suffix);
                            if (logic === undefined) {
                                console.warn(`Could not find suffix lock '${suffix}' for UUID ${uuid}`);
                            }
                            else {
                                this.logicHost.addLogicAsNumber(logic, (v) => {
                                    if (v == 1) {
                                        this.pub.pub('cas_activate_alert', { key: { uuid: uuid, suffix: suffix }, priority: info.priority }, true, false);
                                    }
                                    else {
                                        this.pub.pub('cas_deactivate_alert', { key: { uuid: uuid, suffix: suffix }, priority: info.priority }, true, false);
                                    }
                                }, 0);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Utilities for interacting with charts
     */
    class ChartsClient {
        /**
         * Gets an index of charts for a given airport and provider
         *
         * T = String type representing possible chart types
         * U = Number type representing possible chart types
         * @param airportIcao the FSID (ICAO) of the airport for which to obtain a chart index
         * @param provider the provider for which to query charts. Built-in provider IDs are available on {@link BuiltInChartProvider}
         * @returns a chart index
         */
        static async getIndexForAirport(airportIcao, provider) {
            await ChartsClient.ensureViewListenerReady();
            return new Promise((resolve) => {
                var _a;
                const requestID = ++ChartsClient.nextRequestID;
                const timeoutIndex = setTimeout(() => {
                    ChartsClient.indexRequestMap.delete(requestID);
                    resolve({ airportIcao, charts: [] });
                }, 5000);
                ChartsClient.indexRequestMap.set(requestID, (data) => {
                    clearTimeout(timeoutIndex);
                    resolve(data);
                });
                (_a = ChartsClient.listener) === null || _a === void 0 ? void 0 : _a.call('GET_CHARTS_INDEX', ChartsClient.nextRequestID, airportIcao, provider);
            });
        }
        /**
         * Gets the pages for a chart given its GUID
         * @param chartGuid the GUID of the chart for which to obtain pages
         * @returns the pages for a chart
         */
        static async getChartPages(chartGuid) {
            await ChartsClient.ensureViewListenerReady();
            return new Promise((resolve) => {
                var _a;
                ChartsClient.pagesRequestMap.set(++ChartsClient.nextRequestID, resolve);
                (_a = ChartsClient.listener) === null || _a === void 0 ? void 0 : _a.call('GET_CHART_PAGES', ChartsClient.nextRequestID, chartGuid);
            });
        }
        /**
         * Initializes a chart view with the charts view listener
         *
         * @param view the view to initialize
         */
        static async initializeChartView(view) {
            await ChartsClient.ensureViewListenerReady();
            view.init(ChartsClient.listener);
        }
        /**
         * Setups up the view listener for charts
         */
        static async setupViewListener() {
            ChartsClient.listener = RegisterViewListener('JS_LISTENER_CHARTS', () => ChartsClient.ready.set(true));
            await Wait.awaitSubscribable(ChartsClient.ready, SubscribableMapFunctions.identity(), false, 10000);
            ChartsClient.listener.on('SendChartIndex', ChartsClient.onChartIndexReceived.bind(ChartsClient));
            ChartsClient.listener.on('SendChartPages', ChartsClient.onChartPagesReceived.bind(ChartsClient));
        }
        /**
         * Ensures that the charts view listener is ready
         */
        static async ensureViewListenerReady() {
            if (!ChartsClient.listener) {
                await ChartsClient.setupViewListener();
                return;
            }
            await Wait.awaitSubscribable(ChartsClient.ready, SubscribableMapFunctions.identity(), true, 10000);
        }
        /**
         * Callback that handles when a chart index is received from the simulator
         * @param requestID the request ID
         * @param index the index object
         */
        static onChartIndexReceived(requestID, index) {
            const request = ChartsClient.indexRequestMap.get(requestID);
            if (request) {
                ChartsClient.indexRequestMap.delete(requestID);
                request(index);
            }
            else {
                console.error(`[Charts](onChartIndexReceived) Unknown request: ${requestID}`);
            }
        }
        /**
         * Callback that handles when a chart pages object is received from the simulator
         * @param requestID the request ID
         * @param pages the pages object
         */
        static onChartPagesReceived(requestID, pages) {
            const request = ChartsClient.pagesRequestMap.get(requestID);
            if (request) {
                ChartsClient.pagesRequestMap.delete(requestID);
                request(pages);
            }
            else {
                console.error(`[Charts](onChartPagesReceived) Unknown request: ${requestID}`);
            }
        }
    }
    ChartsClient.listener = null;
    ChartsClient.ready = Subject.create(false);
    ChartsClient.indexRequestMap = new Map();
    ChartsClient.pagesRequestMap = new Map();
    ChartsClient.nextRequestID = 0;

    exports.ChartProcedureType = void 0;
    (function (ChartProcedureType) {
        ChartProcedureType[ChartProcedureType["Sid"] = 0] = "Sid";
        ChartProcedureType[ChartProcedureType["Star"] = 1] = "Star";
        ChartProcedureType[ChartProcedureType["Approach"] = 2] = "Approach";
    })(exports.ChartProcedureType || (exports.ChartProcedureType = {}));
    /**
     * Chart providers built into the simulator
     */
    exports.BuiltInChartProvider = void 0;
    (function (BuiltInChartProvider) {
        BuiltInChartProvider["Lido"] = "LIDO";
        BuiltInChartProvider["Faa"] = "FAA";
    })(exports.BuiltInChartProvider || (exports.BuiltInChartProvider = {}));

    /**
     * A chart view.
     *
     * Chart views represent a single updatable reference that can be updated to show a particular chart image.
     *
     * The sim's engine manages downloading chart images and uploading them as GPU bitmaps. You must use the {@link showChartImage}
     * method to choose which chart image should be shown for this view, after which {@link liveViewName} will be updated to
     * give you access to that image. See documentation of that field for more information.
     */
    class ChartView {
        constructor() {
            this.isAlive = true;
            this.id = null;
            this.listener = null;
            this._liveViewName = Subject.create('');
            /**
             * The name of the LiveView currently backing thiDs chart view.
             *
             * Setting the `src` attribute of an `img` tag to this string will render the last chart selected via `showChartPageUrl` as
             * the image bitmap.
             *
             * You can also use that string as a `background-image` url in CSS.
             */
            this.liveViewName = this._liveViewName;
            this.onSendLiveViewName = (guid, liveViewName) => {
                if (this.id === guid) {
                    this._liveViewName.set(liveViewName);
                }
            };
        }
        /**
         * Initializes this chart view with a listener. This must be a JS_LISTENER_CHARTS ViewListener.
         *
         * @param listener the listener
         */
        async init(listener) {
            var _a;
            if (this.listener) {
                throw new Error('[ChartView](init) Cannot call init if the view is already initialized');
            }
            if (!this.isAlive) {
                throw new Error('[ChartView](init) Cannot call init on a destroyed chart view');
            }
            this.listener = listener;
            this.id = await ((_a = this.listener) === null || _a === void 0 ? void 0 : _a.call('CREATE_CHART_VIEW'));
            this.listener.on('SendLiveViewName', this.onSendLiveViewName);
        }
        /**
         * Updates this chart view to show a new chart image.
         *
         * The URLs passed to this method must come from the {@link ChartPage.url} property, and point to a file in PNG format.
         * Any URL that does not meet this criteria will be ignored.
         *
         * **Note:** you must subscribe to {@link liveViewName} in order to get the new image source. The previous image source is destroyed
         * when a new chart image is shown.
         *
         * @throws if the view is not initialized or has been destroyed
         *
         * @param pageUrl the URL of the image to show.
         */
        showChartImage(pageUrl) {
            if (!this.listener) {
                throw new Error('[ChartView](showChartImage) Cannot call showChartImage before the view is initialized. Did you call init()?');
            }
            if (!this.isAlive) {
                throw new Error('[ChartView](showChartImage) Cannot call showChartImage on a destroyed chart view');
            }
            this.listener.call('SET_CHART_VIEW_URL', this.id, pageUrl);
        }
        /** @inheritdoc */
        destroy() {
            var _a, _b;
            if (!this.isAlive) {
                throw new Error('[ChartView](destroy) View is already destroyed');
            }
            (_a = this.listener) === null || _a === void 0 ? void 0 : _a.off('SendLiveViewName', this.onSendLiveViewName);
            (_b = this.listener) === null || _b === void 0 ? void 0 : _b.call('DESTROY_CHART_VIEW', this.id);
            this.listener = null;
            this.isAlive = false;
        }
    }

    /**
     * A controller for a checklist set. The controller provides a convenient interface for publishing the control
     * events defined in `ChecklistControlEvents`.
     */
    class ChecklistController {
        /**
         * Creates a new instance of ChecklistController.
         * @param index The index of the checklist set on which this controller operates.
         * @param bus The event bus.
         */
        constructor(index, bus) {
            this.index = index;
            if (!Number.isInteger(index) || index < 1) {
                throw new Error(`ChecklistController: invalid index ${index} (must be a positive integer)`);
            }
            this.eventTopicMap = {
                'checklist_reset_all': `checklist_reset_all_${index}`,
                'checklist_reset_group': `checklist_reset_group_${index}`,
                'checklist_reset_list': `checklist_reset_list_${index}`,
                'checklist_reset_item': `checklist_reset_item_${index}`,
                'checklist_complete_list': `checklist_complete_list_${index}`,
                'checklist_complete_item': `checklist_complete_item_${index}`,
                'checklist_toggle_item': `checklist_toggle_item_${index}`
            };
            this.publisher = bus.getPublisher();
        }
        /**
         * Resets all checklists in the checklist set.
         */
        resetAll() {
            this.publisher.pub(this.eventTopicMap['checklist_reset_all'], undefined, true, false);
        }
        /**
         * Resets all checklists in a single checklist group.
         * @param groupIndex The index of the group to reset.
         */
        resetGroup(groupIndex) {
            this.publisher.pub(this.eventTopicMap['checklist_reset_group'], groupIndex, true, false);
        }
        /**
         * Resets all actionable items in a single checklist.
         * @param groupIndex The index of the checklist group containing the checklist to reset.
         * @param listIndex The index of the checklist to reset within its group.
         */
        resetList(groupIndex, listIndex) {
            this.publisher.pub(this.eventTopicMap['checklist_reset_list'], [groupIndex, listIndex], true, false);
        }
        /**
         * Completes all actionable items in a single checklist.
         * @param groupIndex The index of the checklist group containing the checklist to complete.
         * @param listIndex The index of the checklist to complete within its group.
         */
        completeList(groupIndex, listIndex) {
            this.publisher.pub(this.eventTopicMap['checklist_complete_list'], [groupIndex, listIndex], true, false);
        }
        /**
         * Toggles whether an actionable item is completed.
         * @param groupIndex The index of the checklist group containing the item to toggle.
         * @param listIndex The index of the checklist containing the item to toggle within its group.
         * @param branchIndex The index of the branch containing the item to toggle within its list, or `-1` if the item is
         * not contained in a branch.
         * @param itemIndex The index of the item to toggle within its checklist.
         * @param force The state to force onto the item. If `true`, then the item will be completed. If `false`, then the
         * item will be reset. If `undefined`, then the item will be toggled to the opposite of its current state.
         */
        toggleItem(groupIndex, listIndex, branchIndex, itemIndex, force) {
            const topic = force === undefined
                ? 'checklist_toggle_item'
                : force === true
                    ? 'checklist_complete_item'
                    : 'checklist_reset_item';
            this.publisher.pub(this.eventTopicMap[topic], [groupIndex, listIndex, branchIndex, itemIndex], true, false);
        }
    }

    /**
     * Checklist item types.
     */
    exports.ChecklistItemType = void 0;
    (function (ChecklistItemType) {
        ChecklistItemType["Actionable"] = "Actionable";
        ChecklistItemType["Branch"] = "Branch";
        ChecklistItemType["Note"] = "Note";
        ChecklistItemType["Title"] = "Title";
        ChecklistItemType["Link"] = "Link";
        ChecklistItemType["Spacer"] = "Spacer";
    })(exports.ChecklistItemType || (exports.ChecklistItemType = {}));
    /**
     * Completion logic applied to branches linked to checklist branch items.
     */
    exports.ChecklistBranchItemLogicType = void 0;
    (function (ChecklistBranchItemLogicType) {
        /** The completion state of the linked branch has no effect on the completion state of the branch item. */
        ChecklistBranchItemLogicType[ChecklistBranchItemLogicType["None"] = 0] = "None";
        /** The branch item is considered to be completed if the linked branch is completed. */
        ChecklistBranchItemLogicType[ChecklistBranchItemLogicType["Sufficient"] = 1] = "Sufficient";
        /** The branch item is considered to be completed only if the linked branch is completed. */
        ChecklistBranchItemLogicType[ChecklistBranchItemLogicType["Necessary"] = 2] = "Necessary";
    })(exports.ChecklistBranchItemLogicType || (exports.ChecklistBranchItemLogicType = {}));

    /**
     * A parser of checklist definitions from DOM elements.
     * @template I A map from checklist item types to checklist item definitions to which the definitions parsed by the
     * parser conform. Defaults to `ChecklistItemTypeDefMap`. Subclasses that parse item definitions that do not conform to
     * to the default map should override this type parameter as appropriate.
     */
    class ChecklistDOMParser {
        // eslint-disable-next-line jsdoc/require-jsdoc
        parse(element, options) {
            var _a, _b, _c, _d, _e;
            if (element.tagName !== 'Checklist') {
                throw new Error(`ChecklistDOMParser: expected root element tag name Checklist but was ${element.tagName} instead`);
            }
            const optionsToUse = {
                parseSetMetadata: options === null || options === void 0 ? void 0 : options.parseSetMetadata,
                parseGroupMetadata: options === null || options === void 0 ? void 0 : options.parseGroupMetadata,
                parseListMetadata: options === null || options === void 0 ? void 0 : options.parseListMetadata,
                discardEmptyBranches: (_a = options === null || options === void 0 ? void 0 : options.discardEmptyBranches) !== null && _a !== void 0 ? _a : false,
                discardEmptyLists: (_b = options === null || options === void 0 ? void 0 : options.discardEmptyLists) !== null && _b !== void 0 ? _b : false,
                discardEmptyGroups: (_c = options === null || options === void 0 ? void 0 : options.discardEmptyGroups) !== null && _c !== void 0 ? _c : false,
                onError: this.onError.bind(this, (_d = options === null || options === void 0 ? void 0 : options.errorInsteadOfDiscard) !== null && _d !== void 0 ? _d : false)
            };
            const groups = [];
            for (const groupElement of element.querySelectorAll(':scope>Group')) {
                const groupDef = this.parseGroupDef(groupElement, optionsToUse);
                if (groupDef) {
                    groups.push(groupDef);
                }
            }
            const setDef = {
                groups,
                metadata: (_e = optionsToUse.parseSetMetadata) === null || _e === void 0 ? void 0 : _e.call(optionsToUse, element, { groups }, optionsToUse.onError)
            };
            this.auditLinksInSet(setDef, optionsToUse);
            return setDef;
        }
        /**
         * Parses a checklist group definition from a DOM element.
         * @param element The element to parse.
         * @param options The options to use when parsing.
         * @returns The checklist group definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseGroupDef(element, options) {
            var _a;
            const nameAttr = element.getAttribute('name');
            if (!nameAttr) {
                options.onError('ChecklistDOMParser: a checklist group definition has no defined name.');
                return undefined;
            }
            const name = ChecklistDOMParser.tryParseTextAsJSON(nameAttr);
            const lists = [];
            for (const listElement of element.querySelectorAll(':scope>List')) {
                const listDef = this.parseListDef(listElement, name, options);
                if (listDef) {
                    lists.push(listDef);
                }
            }
            if (options.discardEmptyGroups && lists.length === 0) {
                options.onError(`ChecklistDOMParser: definition for checklist group ${name} has no parseable lists and the discard empty group option is enabled.`);
                return undefined;
            }
            return {
                name,
                lists,
                metadata: (_a = options.parseGroupMetadata) === null || _a === void 0 ? void 0 : _a.call(options, element, { name, lists }, options.onError)
            };
        }
        /**
         * Parses a checklist list definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the list is to be parsed.
         * @param options The options to use when parsing.
         * @returns The checklist list definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseListDef(element, groupName, options) {
            var _a;
            const uid = element.getAttribute('uid');
            const nameAttr = element.getAttribute('name');
            if (!nameAttr) {
                options.onError(`ChecklistDOMParser: a checklist list definition in group ${groupName} has no defined name.`);
                return undefined;
            }
            const name = ChecklistDOMParser.tryParseTextAsJSON(nameAttr);
            const branches = [];
            for (const branchElement of element.querySelectorAll(':scope>Branch')) {
                const branchDef = this.parseBranchDef(branchElement, groupName, name, options);
                if (branchDef) {
                    branches.push(branchDef);
                }
            }
            const items = this.parseItemDefArray(element, groupName, name, undefined, options);
            if (options.discardEmptyLists && items.length === 0) {
                options.onError(`ChecklistDOMParser: definition for checklist list ${name} in group ${groupName} has no parseable items and the discard empty list option is enabled.`);
                return undefined;
            }
            return {
                uid: uid ? uid : undefined,
                name,
                items,
                branches,
                metadata: (_a = options.parseListMetadata) === null || _a === void 0 ? void 0 : _a.call(options, element, { name, items, branches }, options.onError)
            };
        }
        /**
         * Parses a checklist branch definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the branch is to be parsed.
         * @param listName The name of the checklist list for which the branch is to be parsed.
         * @param options The options to use when parsing.
         * @returns The checklist branch definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseBranchDef(element, groupName, listName, options) {
            var _a;
            const uid = element.getAttribute('uid');
            if (!uid) {
                options.onError(`ChecklistDOMParser: a checklist branch definition in list ${listName} (group ${groupName}) has no defined UID.`);
                return undefined;
            }
            const nameAttr = element.getAttribute('name');
            if (!nameAttr) {
                options.onError(`ChecklistDOMParser: a checklist branch definition in list ${listName} (group ${groupName}) has no defined name.`);
                return undefined;
            }
            const name = ChecklistDOMParser.tryParseTextAsJSON(nameAttr);
            const items = this.parseItemDefArray(element, groupName, listName, name, options);
            if (options.discardEmptyBranches && items.length === 0) {
                options.onError(`ChecklistDOMParser: definition for checklist branch ${name} in list ${listName} (group ${groupName}) has no parseable items and the discard empty branch option is enabled.`);
                return undefined;
            }
            return {
                uid,
                name,
                items,
                metadata: (_a = options.parseBranchMetadata) === null || _a === void 0 ? void 0 : _a.call(options, element, { uid, name, items }, options.onError)
            };
        }
        /**
         * Parses an array of item definitions from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item array is to be parsed.
         * @param listName The name of the checklist list for which the item array is to be parsed.
         * @param branchName The name of the checklist branch for which the item array is to be parsed.
         * @param options The options to use when parsing.
         * @returns The checklist item definition array parsed from the specified element.
         */
        parseItemDefArray(element, groupName, listName, branchName, options) {
            const items = [];
            for (const itemElement of element.querySelectorAll(':scope>Item')) {
                const itemDef = this.parseItemDef(itemElement, groupName, listName, branchName, options);
                if (itemDef) {
                    items.push(itemDef);
                }
            }
            return items;
        }
        /**
         * Parses a checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseItemDef(element, groupName, listName, branchName, options) {
            const typeAttr = element.getAttribute('type');
            if (!typeAttr) {
                options.onError(`ChecklistDOMParser: a checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined type.`);
                return undefined;
            }
            switch (typeAttr.toLowerCase()) {
                case 'actionable':
                    return this.parseActionableItemDef(element, groupName, listName, branchName, options);
                case 'branch':
                    return this.parseBranchItemDef(element, groupName, listName, branchName, options);
                case 'link':
                    return this.parseLinkItemDef(element, groupName, listName, branchName, options);
                case 'note':
                    return this.parseNoteItemDef(element, groupName, listName, branchName, options);
                case 'title':
                    return this.parseTitleItemDef(element, groupName, listName, branchName, options);
                case 'spacer':
                    return this.parseSpacerItemDef(element, groupName, listName, branchName, options);
                default:
                    options.onError(`ChecklistDOMParser: unrecognized checklist item type ${typeAttr} in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}).`);
                    return undefined;
            }
        }
        /**
         * Parses an actionable checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The actionable checklist item definition parsed from the specified element, or `undefined` if the
         * definition was discarded.
         */
        parseActionableItemDef(element, groupName, listName, branchName, options) {
            var _a, _b, _c, _d;
            const labelTextContent = (_b = (_a = element.querySelector(':scope>LabelText')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (!labelTextContent) {
                options.onError(`ChecklistDOMParser: an actionable checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined label text.`);
                return undefined;
            }
            const labelText = ChecklistDOMParser.tryParseTextAsJSON(labelTextContent);
            const actionTextContent = (_d = (_c = element.querySelector(':scope>ActionText')) === null || _c === void 0 ? void 0 : _c.textContent) === null || _d === void 0 ? void 0 : _d.trim();
            const actionText = actionTextContent ? ChecklistDOMParser.tryParseTextAsJSON(actionTextContent) : '';
            return {
                type: exports.ChecklistItemType.Actionable,
                labelText,
                actionText
            };
        }
        /**
         * Parses a branch checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The branch checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseBranchItemDef(element, groupName, listName, branchName, options) {
            var _a, _b, _c, _d;
            const textContent = (_b = (_a = element.querySelector(':scope>Text')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (!textContent) {
                options.onError(`ChecklistDOMParser: a branch checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined text.`);
                return undefined;
            }
            const text = ChecklistDOMParser.tryParseTextAsJSON(textContent);
            const branches = [];
            const branchLogic = [];
            for (const branchElement of element.querySelectorAll(':scope>Branch')) {
                const uid = (_c = branchElement.textContent) === null || _c === void 0 ? void 0 : _c.trim();
                if (!uid) {
                    options.onError(`ChecklistDOMParser: missing branch UID for a branch checklist item in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}).`);
                    return undefined;
                }
                let logic;
                switch ((_d = branchElement.getAttribute('logic')) === null || _d === void 0 ? void 0 : _d.toLowerCase()) {
                    case 'none':
                    case undefined:
                        logic = exports.ChecklistBranchItemLogicType.None;
                        break;
                    case 'sufficient':
                        logic = exports.ChecklistBranchItemLogicType.Sufficient;
                        break;
                    case 'necessary':
                        logic = exports.ChecklistBranchItemLogicType.Necessary;
                        break;
                    default:
                        options.onError(`ChecklistDOMParser: invalid branch logic for a branch checklist item (must be "none", "sufficient", or "necessary") in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}).`);
                        return undefined;
                }
                branches.push(uid);
                branchLogic.push(logic);
            }
            return {
                type: exports.ChecklistItemType.Branch,
                branches,
                branchLogic,
                text
            };
        }
        /**
         * Parses a link checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The link checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseLinkItemDef(element, groupName, listName, branchName, options) {
            var _a, _b, _c, _d;
            const target = (_b = (_a = element.querySelector(':scope>Target')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (!target) {
                options.onError(`ChecklistDOMParser: a link checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined target.`);
                return undefined;
            }
            const textContent = (_d = (_c = element.querySelector(':scope>Text')) === null || _c === void 0 ? void 0 : _c.textContent) === null || _d === void 0 ? void 0 : _d.trim();
            const text = textContent === undefined ? undefined : ChecklistDOMParser.tryParseTextAsJSON(textContent);
            return {
                type: exports.ChecklistItemType.Link,
                target,
                text
            };
        }
        /**
         * Parses a note checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The note checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseNoteItemDef(element, groupName, listName, branchName, options) {
            var _a, _b;
            const textContent = (_b = (_a = element.querySelector(':scope>Text')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (!textContent) {
                options.onError(`ChecklistDOMParser: a note checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined text.`);
                return undefined;
            }
            const text = ChecklistDOMParser.tryParseTextAsJSON(textContent);
            return {
                type: exports.ChecklistItemType.Note,
                text
            };
        }
        /**
         * Parses a title checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The title checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseTitleItemDef(element, groupName, listName, branchName, options) {
            var _a, _b;
            const textContent = (_b = (_a = element.querySelector(':scope>Text')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (!textContent) {
                options.onError(`ChecklistDOMParser: a title checklist item definition in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}) has no defined text.`);
                return undefined;
            }
            const text = ChecklistDOMParser.tryParseTextAsJSON(textContent);
            return {
                type: exports.ChecklistItemType.Title,
                text,
            };
        }
        /**
         * Parses a spacer checklist item definition from a DOM element.
         * @param element The element to parse.
         * @param groupName The name of the checklist group for which the item is to be parsed.
         * @param listName The name of the checklist list for which the item is to be parsed.
         * @param branchName The name of the checklist branch for which the item is to be parsed.
         * @param options The options to use when parsing.
         * @returns The spacer checklist item definition parsed from the specified element, or `undefined` if the definition
         * was discarded.
         */
        parseSpacerItemDef(element, groupName, listName, branchName, options) {
            const heightAttr = element.getAttribute('height');
            if (heightAttr === null) {
                return { type: exports.ChecklistItemType.Spacer };
            }
            const height = Number(heightAttr);
            if (!Number.isFinite(height) || height < 0) {
                options.onError(`ChecklistDOMParser: invalid height ${heightAttr} for a spacer checklist item (must be a finite non-negative number) in list ${listName}${branchName === undefined ? '' : `, branch ${branchName}`} (group ${groupName}).`);
                return undefined;
            }
            return {
                type: exports.ChecklistItemType.Spacer,
                height
            };
        }
        /**
         * Audits links contained in a parsed checklist set definition.
         * @param set The set to audit.
         * @param options The options to use when parsing.
         */
        auditLinksInSet(set, options) {
            for (const group of set.groups) {
                this.auditLinksInGroup(set, group, options);
            }
        }
        /**
         * Audits links contained in a parsed checklist group definition.
         * @param set The set containing the group to audit.
         * @param group The group to audit.
         * @param options The options to use when parsing.
         */
        auditLinksInGroup(set, group, options) {
            for (const list of group.lists) {
                this.auditLinksInList(set, group, list, options);
            }
        }
        /**
         * Audits links contained in a parsed checklist list definition.
         * @param set The set containing the group to audit.
         * @param group The group containing the list to audit.
         * @param list The list to audit.
         * @param options The options to use when parsing.
         */
        auditLinksInList(set, group, list, options) {
            for (const item of list.items) {
                this.auditLinksInItem(set, group, list, undefined, item, options);
            }
            for (const branch of list.branches) {
                for (const item of branch.items) {
                    this.auditLinksInItem(set, group, list, undefined, item, options);
                }
            }
        }
        /**
         * Audits links contained in a parsed checklist item definition.
         * @param set The set containing the group to audit.
         * @param group The group containing the item to audit.
         * @param list The list containing the item to audit.
         * @param branch The branch containing the item to audit, or `undefined` if the item is not in a branch.
         * @param item The item to audit.
         * @param options The options to use when parsing.
         */
        auditLinksInItem(set, group, list, branch, item, options) {
            switch (item.type) {
                case exports.ChecklistItemType.Branch:
                    this.auditBranchItemLinks(set, group, list, branch, item, options);
                    break;
                case exports.ChecklistItemType.Link:
                    this.auditLinkItemTarget(set, group, list, branch, item, options);
                    break;
            }
        }
        /**
         * Audits links contained in a parsed branch checklist item definition.
         * @param set The set containing the group to audit.
         * @param group The group containing the item to audit.
         * @param list The list containing the item to audit.
         * @param branch The branch containing the item to audit, or `undefined` if the item is not in a branch.
         * @param item The item to audit.
         * @param options The options to use when parsing.
         */
        auditBranchItemLinks(set, group, list, branch, item, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        options) {
            for (const branchUid of item.branches) {
                if (list.branches.findIndex(branchDef => branchDef.uid === branchUid) < 0) {
                    console.warn(`ChecklistDOMParser: a branch checklist item definition in list ${list.name}${branch === undefined ? '' : `, branch ${branch.name}`} (group ${group.name}) has an invalid branch link: UID "${branchUid}".`);
                }
            }
        }
        /**
         * Audits links contained in a parsed link checklist item definition.
         * @param set The set containing the group to audit.
         * @param group The group containing the item to audit.
         * @param list The list containing the item to audit.
         * @param branch The branch containing the item to audit, or `undefined` if the item is not in a branch.
         * @param item The item to audit.
         * @param options The options to use when parsing.
         */
        auditLinkItemTarget(set, group, list, branch, item, 
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        options) {
            for (const searchGroup of set.groups) {
                for (const searchList of searchGroup.lists) {
                    if (searchList.uid === item.target) {
                        return;
                    }
                    for (const searchBranch of searchList.branches) {
                        if (searchBranch.uid === item.target) {
                            return;
                        }
                    }
                }
            }
            console.warn(`ChecklistDOMParser: a link checklist item definition in list ${list.name}${branch === undefined ? '' : `, branch ${branch.name}`} (group ${group.name}) has an invalid link target: UID "${item.target}".`);
        }
        /**
         * Processes an error message by either throwing an error or emitting a console warning.
         * @param throwError Whether to throw an error with the message instead of emitting a console warning.
         * @param message The error message.
         * @throws Error if `throwError` is `true`.
         */
        onError(throwError, message) {
            if (throwError) {
                throw new Error(message);
            }
            else {
                console.warn(`${message} ... Discarding the offending definition.`);
            }
        }
        /**
         * Attempts to parse a text string as a JSON-formatted string.
         * @param text The text string to parse.
         * @returns The string parsed from the text string, or the original text string if it did not encode a JSON-formatted
         * string.
         */
        static tryParseTextAsJSON(text) {
            try {
                const parsedText = JSON.parse(text);
                if (typeof parsedText === 'string') {
                    return parsedText;
                }
            }
            catch (_a) {
                // noop
            }
            return text;
        }
    }

    /**
     * A manager for a set of checklists. The manager tracks the state of all actionable items in the checklist set.
     * The manager can respond to requests to change the state of its checklist set made through the event bus via the
     * topics defined in `ChecklistControlEvents` or through direct calls to methods on the manager itself. When the
     * state of its checklist set changes, the manager will publish data to the appropriate topics defined in
     * `ChecklistStateEvents` to describe the changes that occurred.
     */
    class ChecklistManager {
        /**
         * Creates a new instance of ChecklistManager. The new manager is initialized as asleep.
         * @param index The index of this manager's checklist set.
         * @param bus The event bus.
         * @param checklistDef The definition for this manager's checklist set.
         */
        constructor(index, bus, checklistDef) {
            this.index = index;
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.isAlive = true;
            this.isAwake = false;
            this.subscriptions = [];
            this.controlSubscriptions = [];
            if (!Number.isInteger(index) || index < 1) {
                throw new Error(`ChecklistManager: invalid index ${index} (must be a positive integer)`);
            }
            this.eventTopicMap = {
                'checklist_state_all_reset': `checklist_state_all_reset_${index}`,
                'checklist_state_group_reset': `checklist_state_group_reset_${index}`,
                'checklist_state_list_reset': `checklist_state_list_reset_${index}`,
                'checklist_state_item_reset': `checklist_state_item_reset_${index}`,
                'checklist_state_list_completed': `checklist_state_list_completed_${index}`,
                'checklist_state_item_completed': `checklist_state_item_completed_${index}`,
                'checklist_state_response': `checklist_state_response_${index}`,
            };
            this.state = this.initState(checklistDef);
            const sub = this.bus.getSubscriber();
            this.controlSubscriptions.push(sub.on(`checklist_reset_all_${index}`).handle(this.resetAll.bind(this), true), sub.on(`checklist_reset_group_${index}`).handle(this.resetGroup.bind(this), true), sub.on(`checklist_reset_list_${index}`).handle(([groupIndex, listIndex]) => { this.resetList(groupIndex, listIndex); }, true), sub.on(`checklist_reset_item_${index}`).handle(([groupIndex, listIndex, branchIndex, itemIndex]) => { this.toggleItem(groupIndex, listIndex, branchIndex, itemIndex, false); }, true), sub.on(`checklist_complete_list_${index}`).handle(([groupIndex, listIndex]) => { this.completeList(groupIndex, listIndex); }, true), sub.on(`checklist_complete_item_${index}`).handle(([groupIndex, listIndex, branchIndex, itemIndex]) => { this.toggleItem(groupIndex, listIndex, branchIndex, itemIndex, true); }, true), sub.on(`checklist_toggle_item_${index}`).handle(([groupIndex, listIndex, branchIndex, itemIndex]) => { this.toggleItem(groupIndex, listIndex, branchIndex, itemIndex); }, true));
            this.subscriptions.push(...this.controlSubscriptions, sub.on(`checklist_state_request_${index}`).handle(this.onStateRequest.bind(this)));
        }
        /**
         * Initializes the state of this manager's checklist set.
         * @param checklistDef The definition for the checklist set.
         * @returns The initialized state of this manager's checklist set.
         */
        initState(checklistDef) {
            const state = [];
            for (const groupDef of checklistDef.groups) {
                const groupState = [];
                for (const listDef of groupDef.lists) {
                    const listState = [];
                    // base list
                    listState.push(this.initBranchState(listDef.items));
                    // branches
                    for (let i = 0; i < listDef.branches.length; i++) {
                        listState.push(this.initBranchState(listDef.branches[i].items));
                    }
                    groupState.push(listState);
                }
                state.push(groupState);
            }
            return state;
        }
        /**
         * Initializes the state of a checklist branch.
         * @param items The definitions for the items in the branch.
         * @returns The initialized state of the checklist branch with the specified items.
         */
        initBranchState(items) {
            const state = [];
            for (let i = 0; i < items.length; i++) {
                if (items[i].type === exports.ChecklistItemType.Actionable || items[i].type === exports.ChecklistItemType.Branch) {
                    state[i] = false;
                }
            }
            return state;
        }
        /**
         * Wakes this manager. When awake, this manager will respond to commands to change the state of its checklist set.
         * @throws Error if this manager has been destroyed.
         */
        wake() {
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot wake a dead manager');
            }
            if (this.isAwake) {
                return;
            }
            this.isAwake = true;
            for (const sub of this.controlSubscriptions) {
                sub.resume();
            }
        }
        /**
         * Puts this manager to sleep. When asleep, this manager will not respond to commands to change the state of its
         * checklist set. Sleeping managers still respond to checklist state requests.
         * @throws Error if this manager has been destroyed.
         */
        sleep() {
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            this.isAwake = false;
            for (const sub of this.controlSubscriptions) {
                sub.pause();
            }
        }
        /**
         * Resets all checklists in this manager's checklist set. Has no effect if this manager is asleep.
         * @throws Error if this manager has been destroyed.
         */
        resetAll() {
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            for (let groupIndex = 0; groupIndex < this.state.length; groupIndex++) {
                const groupState = this.state[groupIndex];
                for (let listIndex = 0; listIndex < groupState.length; listIndex++) {
                    const listState = groupState[listIndex];
                    for (let branchIndex = 0; branchIndex < listState.length; branchIndex++) {
                        const branchState = listState[branchIndex];
                        for (let itemIndex = 0; itemIndex < branchState.length; itemIndex++) {
                            if (branchState[itemIndex] === true) {
                                branchState[itemIndex] = false;
                            }
                        }
                    }
                }
            }
            this.publisher.pub(this.eventTopicMap['checklist_state_all_reset'], undefined, true, false);
        }
        /**
         * Resets all checklists in a single checklist group. Has no effect if this manager is asleep.
         * @param groupIndex The index of the group to reset.
         * @throws Error if this manager has been destroyed.
         */
        resetGroup(groupIndex) {
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            const groupState = this.state[groupIndex];
            if (!groupState) {
                return;
            }
            for (let listIndex = 0; listIndex < groupState.length; listIndex++) {
                const listState = groupState[listIndex];
                for (let branchIndex = 0; branchIndex < listState.length; branchIndex++) {
                    const branchState = listState[branchIndex];
                    for (let itemIndex = 0; itemIndex < branchState.length; itemIndex++) {
                        if (branchState[itemIndex] === true) {
                            branchState[itemIndex] = false;
                        }
                    }
                }
            }
            this.publisher.pub(this.eventTopicMap['checklist_state_group_reset'], groupIndex, true, false);
        }
        /**
         * Resets all actionable items in a single checklist. Has no effect if this manager is asleep.
         * @param groupIndex The index of the checklist group containing the checklist to reset.
         * @param listIndex The index of the checklist to reset within its group.
         * @throws Error if this manager has been destroyed.
         */
        resetList(groupIndex, listIndex) {
            var _a;
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            const listState = (_a = this.state[groupIndex]) === null || _a === void 0 ? void 0 : _a[listIndex];
            if (!listState) {
                return;
            }
            for (let branchIndex = 0; branchIndex < listState.length; branchIndex++) {
                const branchState = listState[branchIndex];
                for (let itemIndex = 0; itemIndex < branchState.length; itemIndex++) {
                    if (branchState[itemIndex] === true) {
                        branchState[itemIndex] = false;
                    }
                }
            }
            this.publisher.pub(this.eventTopicMap['checklist_state_list_reset'], [groupIndex, listIndex], true, false);
        }
        /**
         * Completes all actionable items in a single checklist. Has no effect if this manager is asleep.
         * @param groupIndex The index of the checklist group containing the checklist to complete.
         * @param listIndex The index of the checklist to complete within its group.
         * @throws Error if this manager has been destroyed.
         */
        completeList(groupIndex, listIndex) {
            var _a;
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            const listState = (_a = this.state[groupIndex]) === null || _a === void 0 ? void 0 : _a[listIndex];
            if (!listState) {
                return;
            }
            for (let branchIndex = 0; branchIndex < listState.length; branchIndex++) {
                const branchState = listState[branchIndex];
                for (let itemIndex = 0; itemIndex < branchState.length; itemIndex++) {
                    if (branchState[itemIndex] === false) {
                        branchState[itemIndex] = true;
                    }
                }
            }
            this.publisher.pub(this.eventTopicMap['checklist_state_list_completed'], [groupIndex, listIndex], true, false);
        }
        /**
         * Toggles whether an actionable item is completed. Has no effect if this manager is asleep.
         * @param groupIndex The index of the checklist group containing the item to toggle.
         * @param listIndex The index of the checklist containing the item to toggle within its group.
         * @param branchIndex The index of the branch containing the item to toggle within its list, or `-1` if the item is
         * not contained in a branch.
         * @param itemIndex The index of the item to toggle within its checklist.
         * @param force The state to force onto the item. If `true`, then the item will be completed. If `false`, then the
         * item will be reset. If `undefined`, then the item will be toggled to the opposite of its current state.
         * @throws Error if this manager has been destroyed.
         */
        toggleItem(groupIndex, listIndex, branchIndex, itemIndex, force) {
            var _a, _b;
            if (!this.isAlive) {
                throw new Error('ChecklistManager: cannot sleep a dead manager');
            }
            if (!this.isAwake) {
                return;
            }
            branchIndex = branchIndex < 0 ? -1 : branchIndex;
            const branchState = (_b = (_a = this.state[groupIndex]) === null || _a === void 0 ? void 0 : _a[listIndex]) === null || _b === void 0 ? void 0 : _b[branchIndex + 1];
            if (!branchState) {
                return;
            }
            const toggledState = this.tryToggleItemCompleted(branchState, itemIndex, force);
            if (toggledState !== undefined) {
                this.publisher.pub(this.eventTopicMap[toggledState ? 'checklist_state_item_completed' : 'checklist_state_item_reset'], [groupIndex, listIndex, branchIndex, itemIndex], true, false);
            }
        }
        /**
         * Attempts to toggle whether an actionable item is completed.
         * @param branchState The state array of the checklist containing the item to toggle.
         * @param itemIndex The index of the item to toggle within its checklist.
         * @param force The state to force onto the item. If `true`, then the item will be completed. If `false`, then the
         * item will be reset. If `undefined`, then the item will be toggled to the opposite of its current state.
         * @returns The state of the item after the operation is complete: `true` indicates the item was completed, `false`
         * indicates the item was reset, and `undefined` indicates that the state of the item could not be toggled (either
         * because the item does not exist or because it is not an actionable item).
         */
        tryToggleItemCompleted(branchState, itemIndex, force) {
            const currentState = branchState[itemIndex];
            if (currentState !== undefined && (force === undefined || currentState !== force)) {
                branchState[itemIndex] = force !== null && force !== void 0 ? force : !currentState;
                return branchState[itemIndex];
            }
            else {
                return undefined;
            }
        }
        /**
         * Responds to when a state request is received.
         * @param uuid The UUID of the state request.
         */
        onStateRequest(uuid) {
            var _a, _b, _c;
            const state = ArrayUtils.create(this.state.length, () => null);
            for (let groupIndex = 0; groupIndex < this.state.length; groupIndex++) {
                const groupState = this.state[groupIndex];
                for (let listIndex = 0; listIndex < groupState.length; listIndex++) {
                    const listState = groupState[listIndex];
                    for (let branchIndex = 0; branchIndex < listState.length; branchIndex++) {
                        const branchState = listState[branchIndex];
                        for (let itemIndex = 0; itemIndex < branchState.length; itemIndex++) {
                            if (branchState[itemIndex] === true) {
                                const groupArray = (_a = state[groupIndex]) !== null && _a !== void 0 ? _a : (state[groupIndex] = ArrayUtils.create(groupState.length, () => null));
                                const listArray = (_b = groupArray[listIndex]) !== null && _b !== void 0 ? _b : (groupArray[listIndex] = ArrayUtils.create(listState.length, () => null));
                                const branchArray = (_c = listArray[branchIndex]) !== null && _c !== void 0 ? _c : (listArray[branchIndex] = []);
                                branchArray.push(itemIndex);
                            }
                        }
                    }
                }
            }
            this.publisher.pub(this.eventTopicMap['checklist_state_response'], { uuid, state }, true, false);
        }
        /**
         * Destroys this manager. Once destroyed, the manager will no longer track the state of its checklist set, will no
         * longer respond to commands to change the state, and will no longer respond to state requests.
         */
        destroy() {
            this.isAlive = false;
            for (const sub of this.subscriptions) {
                sub.destroy();
            }
        }
    }

    /**
     * A default implementation of {@link ChecklistStateProvider}.
     * @template I A map from checklist item types to checklist item definitions used by the checklists contained in the
     * provider's checklist set. Defaults to `BaseChecklistItemTypeDefMap`.
     * @template S The metadata attached to the provider's checklist set.
     * @template G The metadata attached to the checklist groups contained in the provider's checklist set.
     * @template L The metadata attached to the checklist lists contained in the provider's checklist set.
     * @template B The metadata attached to the checklist branches contained in the provider's checklist set.
     */
    class DefaultChecklistStateProvider {
        /**
         * Creates a new instance of DefaultChecklistStateProvider. The provider is created in an uninitialized state and
         * must be initialized by calling `init()` before it can update its data.
         * @param index The index of this provider's checklist set.
         * @param bus The event bus.
         * @param checklistSetDef The definition for this provider's checklist set.
         */
        constructor(index, bus, checklistSetDef) {
            this.index = index;
            this.bus = bus;
            this.lastStateRequestUuid = undefined;
            this.isAlive = true;
            this.isInit = false;
            this.isResumed = false;
            this.subscriptions = [];
            this._state = {
                groups: checklistSetDef.groups.map(groupDef => {
                    return {
                        name: groupDef.name,
                        lists: groupDef.lists.map(DefaultChecklistStateProvider.parseList),
                        metadata: groupDef.metadata
                    };
                }),
                metadata: checklistSetDef.metadata
            };
            this.state = this._state;
        }
        /**
         * Parses a checklist list state from a definition.
         * @param listDef The definition to parse.
         * @returns A checklist list state for the specified definition.
         */
        static parseList(listDef) {
            const list = {
                uid: listDef.uid,
                name: listDef.name,
                items: listDef.items.map(DefaultChecklistStateProvider.parseItem),
                branches: listDef.branches.map(DefaultChecklistStateProvider.parseBranch),
                branchDependents: DefaultChecklistStateProvider.parseListBranchDependencies(listDef),
                metadata: listDef.metadata,
                isCompleted: Subject.create(false)
            };
            DefaultChecklistStateProvider.resolveBranchItems(list, -1);
            for (let branchIndex = 0; branchIndex < list.branches.length; branchIndex++) {
                DefaultChecklistStateProvider.resolveBranchItems(list, branchIndex);
            }
            return list;
        }
        /**
         * Parses branch dependencies from a checklist list definition.
         * @param listDef The definition to parse.
         * @returns Branch dependencies for the specified definition. For each index `i` in the array, `array[i]` is an array
         * of the indexes of all branches that depend on branch `i`, or `null` if no branches depend on branch `i`.
         * @throws Error if a circular dependency is found.
         */
        static parseListBranchDependencies(listDef) {
            const dependencies = [];
            DefaultChecklistStateProvider.parseBranchDependencies(listDef, -1, dependencies);
            for (let branchIndex = 0; branchIndex < listDef.branches.length; branchIndex++) {
                DefaultChecklistStateProvider.parseBranchDependencies(listDef, branchIndex, dependencies);
            }
            // Check for circular dependencies.
            let foundCircularDependencies = false;
            const done = new Set();
            for (let i = 0; i < dependencies.length; i++) {
                const result = DefaultChecklistStateProvider.findCircularDependencies(listDef, dependencies, i, done, new Set());
                foundCircularDependencies || (foundCircularDependencies = result);
            }
            if (foundCircularDependencies) {
                throw new Error('DefaultChecklistStateProvider: found circular branch dependencies');
            }
            return dependencies;
        }
        /**
         * Parses the dependencies of a checklist branch from a checklist list definition and writes the results to a
         * dependencies array.
         * @param listDef The definition to parse.
         * @param branchIndex The index of the branch for which to parse dependencies, or `-1` to parse dependencies for the
         * base checklist list.
         * @param dependencies The dependencies array to which to write the results. For each index `i` in the array,
         * `array[i]` is an array of the indexes of all branches that depend on branch `i`, or `null` if no branches depend
         * on branch `i`.
         */
        static parseBranchDependencies(listDef, branchIndex, dependencies) {
            var _a, _b, _c;
            const items = (_b = (_a = listDef.branches[branchIndex]) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : listDef.items;
            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                if (item.type === exports.ChecklistItemType.Branch) {
                    for (let i = 0; i < item.branches.length; i++) {
                        const dependencyBranchIndex = listDef.branches.findIndex(branchDef => branchDef.uid === item.branches[i]);
                        if (listDef.branches[dependencyBranchIndex]) {
                            const branchDependencies = (_c = dependencies[dependencyBranchIndex]) !== null && _c !== void 0 ? _c : (dependencies[dependencyBranchIndex] = []);
                            if (!branchDependencies.includes(branchIndex)) {
                                branchDependencies.push(branchIndex);
                            }
                        }
                    }
                }
            }
        }
        /**
         * Checks whether the dependency tree of a checklist branch includes one or more circular dependencies.
         * @param listDef The definition of the checklist list containing the branch to check.
         * @param dependencies The branch dependencies array for the checklist. For each index `i` in the array, `array[i]`
         * is an array of the indexes of all branches that depend on branch `i`, or `null` if no branches depend on branch
         * `i`.
         * @param index The index of the branch to check.
         * @param done A set of the indexes of all branches whose dependency trees have already been checked.
         * @param seen A set of the indexes of all branches that have already been visited in the current dependency chain.
         * @returns Whether a circular dependency was found.
         */
        static findCircularDependencies(listDef, dependencies, index, done, seen) {
            if (done.has(index)) {
                return false;
            }
            if (seen.has(index)) {
                console.warn(`DefaultChecklistStateProvider: circular branch dependency found in list ${listDef.name}, dependency chain (branch indexes): ${Array.from(seen).join(' -> ')}`);
                done.add(index);
                return true;
            }
            seen.add(index);
            let found = false;
            const branchDependencies = dependencies[index];
            if (branchDependencies) {
                for (let i = 0; i < branchDependencies.length; i++) {
                    const result = DefaultChecklistStateProvider.findCircularDependencies(listDef, dependencies, branchDependencies[i], done, new Set(seen));
                    found || (found = result);
                }
            }
            done.add(index);
            return found;
        }
        /**
         * Resolves the index and completion state references of the linked branches for all branch items in a checklist
         * branch.
         * @param list The checklist containing the branch items to resolve.
         * @param branchIndex The index of the branch containing the branch items to resolve, or `-1` to resolve items in the
         * base checklist.
         */
        static resolveBranchItems(list, branchIndex) {
            var _a, _b;
            const items = (_b = (_a = list.branches[branchIndex]) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : list.items;
            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                if (item.type === exports.ChecklistItemType.Branch) {
                    for (let i = 0; i < item.def.branches.length; i++) {
                        const linkedBranchIndex = list.branches.findIndex(branch => branch.uid === item.def.branches[i]);
                        const linkedBranch = list.branches[linkedBranchIndex];
                        if (linkedBranch) {
                            item.branchIndexes[i] = linkedBranchIndex;
                            item.isBranchCompleted[i] = linkedBranch.isCompleted;
                        }
                        else {
                            item.branchIndexes[i] = -1;
                            item.isBranchCompleted[i] = Subject.create(false);
                        }
                    }
                }
            }
        }
        /**
         * Parses a checklist branch state from a definition.
         * @param branchDef The definition to parse.
         * @returns A checklist branch state for the specified definition.
         */
        static parseBranch(branchDef) {
            return {
                uid: branchDef.uid,
                name: branchDef.name,
                items: branchDef.items.map(DefaultChecklistStateProvider.parseItem),
                metadata: branchDef.metadata,
                isCompleted: Subject.create(false)
            };
        }
        /**
         * Parses a checklist item from a definition.
         * @param itemDef The definition to parse.
         * @returns A checklist item for the specified definition.
         */
        static parseItem(itemDef) {
            switch (itemDef.type) {
                case exports.ChecklistItemType.Actionable:
                    return {
                        type: itemDef.type,
                        def: itemDef,
                        isCompleted: Subject.create(false)
                    };
                case exports.ChecklistItemType.Branch:
                    return {
                        type: itemDef.type,
                        def: itemDef,
                        branchIndexes: [],
                        isCompleted: Subject.create(false),
                        isOverridden: Subject.create(false),
                        isBranchCompleted: []
                    };
                default:
                    return {
                        type: itemDef.type,
                        def: itemDef
                    };
            }
        }
        /**
         * Initializes this provider. Once initialized, this provider will continuously update its data until paused or
         * destroyed.
         * @param paused Whether to initialize this provider as paused. Defaults to `false`.
         * @throws Error if this provider has been destroyed.
         */
        init(paused = false) {
            if (!this.isAlive) {
                throw new Error('DefaultChecklistStateProvider: cannot initialize a dead provider');
            }
            if (this.isInit) {
                return;
            }
            this.isInit = true;
            const sub = this.bus.getSubscriber();
            this.subscriptions.push(sub.on(`checklist_state_all_reset_${this.index}`).handle(this.onAllReset.bind(this)), sub.on(`checklist_state_group_reset_${this.index}`).handle(this.onGroupReset.bind(this)), sub.on(`checklist_state_list_reset_${this.index}`).handle(this.onListCompletedSet.bind(this, false)), sub.on(`checklist_state_list_completed_${this.index}`).handle(this.onListCompletedSet.bind(this, true)), sub.on(`checklist_state_item_reset_${this.index}`).handle(this.onItemCompletedSet.bind(this, false)), sub.on(`checklist_state_item_completed_${this.index}`).handle(this.onItemCompletedSet.bind(this, true)), sub.on(`checklist_state_response_${this.index}`).handle(this.onStateResponse.bind(this)));
            if (!paused) {
                this.resume();
            }
        }
        /**
         * Resumes this provider. Once resumed, this provider will continuously update its data until paused or destroyed.
         * @throws Error if this provider has been destroyed.
         */
        resume() {
            if (!this.isAlive) {
                throw new Error('DefaultChecklistStateProvider: cannot resume a dead provider');
            }
            if (!this.isInit || this.isResumed) {
                return;
            }
            this.isResumed = true;
            this.bus.getPublisher().pub(`checklist_state_request_${this.index}`, this.lastStateRequestUuid = UUID.GenerateUuid(), true, false);
            for (const sub of this.subscriptions) {
                sub.resume();
            }
        }
        /**
         * Pauses this provider. Once paused, this provider will not update its data until it is resumed.
         * @throws Error if this provider has been destroyed.
         */
        pause() {
            if (!this.isAlive) {
                throw new Error('DefaultChecklistStateProvider: cannot pause a dead provider');
            }
            if (!this.isInit || !this.isResumed) {
                return;
            }
            this.isResumed = false;
            for (const sub of this.subscriptions) {
                sub.pause();
            }
        }
        /**
         * Responds to when all checklist groups have been reset.
         */
        onAllReset() {
            for (let groupIndex = 0; groupIndex < this._state.groups.length; groupIndex++) {
                this.onGroupReset(groupIndex);
            }
        }
        /**
         * Responds to when a single checklist group has been reset.
         * @param groupIndex The index of the reset group.
         */
        onGroupReset(groupIndex) {
            const group = this._state.groups[groupIndex];
            if (!group) {
                return;
            }
            const indexes = [groupIndex, -1];
            for (let listIndex = 0; listIndex < group.lists.length; listIndex++) {
                indexes[1] = listIndex;
                this.onListCompletedSet(false, indexes);
            }
        }
        /**
         * Responds to when all actionable items in a single checklist have been reset or completed.
         * @param completed Whether items were completed.
         * @param data The indexes of the affected checklist, as an ordered tuple of: the index of the checklist group
         * containing the checklist, and the index of the checklist within its group.
         */
        onListCompletedSet(completed, data) {
            var _a;
            const list = (_a = this._state.groups[data[0]]) === null || _a === void 0 ? void 0 : _a.lists[data[1]];
            if (!list) {
                return;
            }
            // First, set the completion state of every actionable item and override state of every branch item in the list and
            // all branches.
            for (let itemIndex = 0; itemIndex < list.items.length; itemIndex++) {
                const item = list.items[itemIndex];
                switch (item.type) {
                    case exports.ChecklistItemType.Actionable:
                        item.isCompleted.set(completed);
                        break;
                    case exports.ChecklistItemType.Branch:
                        item.isOverridden.set(completed);
                        break;
                }
            }
            for (let branchIndex = 0; branchIndex < list.branches.length; branchIndex++) {
                const branch = list.branches[branchIndex];
                for (let itemIndex = 0; itemIndex < branch.items.length; itemIndex++) {
                    const item = branch.items[itemIndex];
                    switch (item.type) {
                        case exports.ChecklistItemType.Actionable:
                            item.isCompleted.set(completed);
                            break;
                        case exports.ChecklistItemType.Branch:
                            item.isOverridden.set(completed);
                            break;
                    }
                }
            }
            // Next, update the completion state of the list and all of its branches.
            const done = new Set();
            this.updateBranchCompletionStateDown(list, -1, done);
            for (let i = 0; i < list.branches.length; i++) {
                this.updateBranchCompletionStateDown(list, i, done);
            }
        }
        /**
         * Responds to when an actionable item has been reset or completed or when a branch item has been reset or
         * overridden.
         * @param completed Whether the item was completed/overridden.
         * @param data The indexes of the affected item, as an ordered tuple of: the index of the checklist group containing
         * the item, the index of the checklist containing the item within its group, the index of the branch containing the
         * item within its checklist (or `-1` if the item is in the base checklist), and the index of the item within its
         * checklist or branch.
         */
        onItemCompletedSet(completed, data) {
            var _a;
            const [groupIndex, listIndex, branchIndex, itemIndex] = data;
            const list = (_a = this._state.groups[groupIndex]) === null || _a === void 0 ? void 0 : _a.lists[listIndex];
            if (!list) {
                return;
            }
            if (branchIndex >= list.branches.length) {
                return;
            }
            const branch = list.branches[branchIndex];
            const items = branch ? branch.items : list.items;
            const item = items[itemIndex];
            if (!item) {
                return;
            }
            const isCompletedSubject = branch ? branch.isCompleted : list.isCompleted;
            let isItemCompleted = undefined;
            if (item.type === exports.ChecklistItemType.Actionable) {
                if (item.isCompleted.get() !== completed) {
                    item.isCompleted.set(completed);
                    isItemCompleted = completed;
                }
            }
            else if (item.type === exports.ChecklistItemType.Branch) {
                item.isOverridden.set(completed);
                if (completed) {
                    if (!item.isCompleted.get()) {
                        item.isCompleted.set(completed);
                        isItemCompleted = true;
                    }
                }
                else {
                    const isItemCompletedNew = this.isBranchItemCompleted(item);
                    if (isItemCompletedNew !== item.isCompleted.get()) {
                        item.isCompleted.set(isItemCompletedNew);
                        isItemCompleted = isItemCompletedNew;
                    }
                }
            }
            // If the item completion state did not change as a result of the operation, then we are done.
            if (isItemCompleted === undefined) {
                return;
            }
            const wasCompleted = isCompletedSubject.get();
            if (isItemCompleted) {
                // If the item completion state changed to true, then check if no items in the parent list/branch are
                // uncompleted. If so, then the parent list/branch is completed.
                let isCompleted = true;
                for (let i = 0; isCompleted && i < items.length; i++) {
                    const iteratedItem = items[i];
                    switch (iteratedItem.type) {
                        case exports.ChecklistItemType.Actionable:
                        case exports.ChecklistItemType.Branch:
                            if (!iteratedItem.isCompleted.get()) {
                                isCompleted = false;
                            }
                            break;
                    }
                }
                isCompletedSubject.set(isCompleted);
            }
            else {
                // If the item completion state changed to false, then the parent list/branch completion state must be changed
                // to false.
                isCompletedSubject.set(false);
            }
            // If a branch changed completion state, then we need to walk back up the dependency chain and update the
            // completion state of all branches that depend on the one that was changed.
            if (branch && isCompletedSubject.get() !== wasCompleted) {
                const dependents = this.enumerateDependents(list, branchIndex);
                for (let i = 0; i < dependents.length; i++) {
                    this.updateBranchCompletionStateFlat(list, dependents[i]);
                }
            }
        }
        /**
         * Responds to when a checklist state response is received.
         * @param data The response data.
         */
        onStateResponse(data) {
            var _a, _b;
            if (data.uuid !== this.lastStateRequestUuid) {
                return;
            }
            this.lastStateRequestUuid = undefined;
            for (let groupIndex = 0; groupIndex < this._state.groups.length; groupIndex++) {
                const groupState = data.state[groupIndex];
                const group = this._state.groups[groupIndex];
                for (let listIndex = 0; listIndex < group.lists.length; listIndex++) {
                    const listState = groupState ? groupState[listIndex] : null;
                    const list = group.lists[listIndex];
                    // base list
                    this.setBranchStateFromResponse(list.items, (_a = listState === null || listState === void 0 ? void 0 : listState[0]) !== null && _a !== void 0 ? _a : null);
                    // branches
                    for (let branchIndex = 0; branchIndex < list.branches.length; branchIndex++) {
                        this.setBranchStateFromResponse(list.branches[branchIndex].items, (_b = listState === null || listState === void 0 ? void 0 : listState[branchIndex + 1]) !== null && _b !== void 0 ? _b : null);
                    }
                    // Update the completion state for the list and all of its branches.
                    const done = new Set();
                    this.updateBranchCompletionStateDown(list, -1, done);
                    for (let i = 0; i < list.branches.length; i++) {
                        this.updateBranchCompletionStateDown(list, i, done);
                    }
                }
            }
        }
        /**
         * Sets the completion states of actionable items and override states of branch items in a checklist branch to match
         * the data in a checklist state response.
         * @param items The items in the checklist branch to set.
         * @param responseBranchState The branch state in the checklist state response.
         */
        setBranchStateFromResponse(items, responseBranchState) {
            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                switch (item.type) {
                    case exports.ChecklistItemType.Actionable: {
                        const isCompleted = responseBranchState !== null && responseBranchState.includes(itemIndex);
                        item.isCompleted.set(isCompleted);
                        break;
                    }
                    case exports.ChecklistItemType.Branch: {
                        const isOverridden = responseBranchState !== null && responseBranchState.includes(itemIndex);
                        item.isOverridden.set(isOverridden);
                        break;
                    }
                }
            }
        }
        /**
         * Updates the completion state of a checklist branch. This method will recursively update the completion state of
         * any dependencies of the branch that are required to compute the branch's completion state.
         * @param list The checklist list containing the branch to update.
         * @param branchIndex The index of the branch to update, or `-1` to update the base list.
         * @param done A set of the indexes of all branches whose completion states have already been updated.
         */
        updateBranchCompletionStateDown(list, branchIndex, done) {
            var _a;
            if (branchIndex >= list.branches.length) {
                return;
            }
            branchIndex = Math.max(branchIndex, -1);
            if (done.has(branchIndex)) {
                return;
            }
            const branch = list.branches[branchIndex];
            const items = branch ? branch.items : list.items;
            const isCompletedSubject = branch ? branch.isCompleted : list.isCompleted;
            let result = true;
            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                switch (item.type) {
                    case exports.ChecklistItemType.Actionable:
                        if (!item.isCompleted.get()) {
                            result = false;
                        }
                        break;
                    case exports.ChecklistItemType.Branch:
                        if (item.isOverridden.get()) {
                            item.isCompleted.set(true);
                        }
                        else {
                            let sufficientItemCompleted = false;
                            let necessaryItemsCompleted = undefined;
                            for (let i = 0; !(sufficientItemCompleted || !!necessaryItemsCompleted) && i < item.def.branches.length; i++) {
                                const dependencyBranch = list.branches[item.branchIndexes[i]];
                                const dependencyBranchLogic = (_a = item.def.branchLogic[i]) !== null && _a !== void 0 ? _a : exports.ChecklistBranchItemLogicType.None;
                                if (dependencyBranch && dependencyBranchLogic !== exports.ChecklistBranchItemLogicType.None) {
                                    this.updateBranchCompletionStateDown(list, item.branchIndexes[i], done);
                                    if (dependencyBranchLogic === exports.ChecklistBranchItemLogicType.Sufficient) {
                                        sufficientItemCompleted || (sufficientItemCompleted = dependencyBranch.isCompleted.get());
                                    }
                                    else {
                                        necessaryItemsCompleted !== null && necessaryItemsCompleted !== void 0 ? necessaryItemsCompleted : (necessaryItemsCompleted = true);
                                        necessaryItemsCompleted && (necessaryItemsCompleted = dependencyBranch.isCompleted.get());
                                    }
                                }
                            }
                            item.isCompleted.set(sufficientItemCompleted || !!necessaryItemsCompleted);
                        }
                        if (!item.isCompleted.get()) {
                            result = false;
                        }
                        break;
                }
            }
            isCompletedSubject.set(result);
            done.add(branchIndex);
        }
        /**
         * Updates the completion state of a checklist branch. This method only updates the specified branch and will not
         * update the completion state of any dependents or dependencies of the branch.
         * @param list The checklist list containing the branch to update.
         * @param branchIndex The index of the branch to update, or `-1` to update the base list.
         */
        updateBranchCompletionStateFlat(list, branchIndex) {
            if (branchIndex >= list.branches.length) {
                return;
            }
            const branch = list.branches[branchIndex];
            const items = branch ? branch.items : list.items;
            const isCompletedSubject = branch ? branch.isCompleted : list.isCompleted;
            let result = true;
            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                switch (item.type) {
                    case exports.ChecklistItemType.Actionable:
                        if (!item.isCompleted.get()) {
                            result = false;
                        }
                        break;
                    case exports.ChecklistItemType.Branch:
                        if (item.isOverridden.get()) {
                            item.isCompleted.set(true);
                        }
                        else {
                            item.isCompleted.set(this.isBranchItemCompleted(item));
                        }
                        if (!item.isCompleted.get()) {
                            result = false;
                        }
                        break;
                }
            }
            isCompletedSubject.set(result);
        }
        /**
         * Enumerates all of the dependent branches of a checklist branch.
         * @param list The checklist list containing the branch for which to enumerate dependents.
         * @param branchIndex The index of the branch for which to enumerate dependents.
         * @returns An array containing the indexes of all of the dependent branches of the specified checklist branch. An
         * index of `-1` indicates the base checklist. Any given index is guaranteed to appear in the array at most once. The
         * indexes are ordered such that for any two branches A and B, if A is a dependent of B, then A's index will appear
         * after B's index.
         */
        enumerateDependents(list, branchIndex) {
            const dependents = [];
            const branchDependents = list.branchDependents[branchIndex];
            if (!branchDependents) {
                return [];
            }
            // Traverse the dependents graph in breadth-first order, adding to the dependents array as we go. If we end up
            // traversing a branch that was previously traversed, then move the entry for the branch in the array to the end
            // instead of skipping it. Note that we are guaranteed to not have any cycles in the graph (no circular
            // dependencies), so moving previously traversed branches to the end of the array cannot result in an infinite
            // loop.
            for (let i = 0; i < branchDependents.length; i++) {
                const currentBranchIndex = branchDependents[i];
                if (currentBranchIndex < 0 || list.branches[currentBranchIndex]) {
                    dependents.push(currentBranchIndex);
                }
            }
            let currentIndex = 0;
            while (currentIndex < dependents.length) {
                const currentBranchIndex = dependents[currentIndex];
                const currentBranch = list.branches[currentBranchIndex];
                // If currentBranch is undefined, then currentBranchIndex must be -1 (we don't allow positive invalid indexes to
                // be added to the array). Since -1 indicates the base checklist and the base checklist cannot have any
                // dependents, we will only search for dependents when currentBranch is defined.
                if (currentBranch) {
                    const currentBranchDependents = list.branchDependents[currentBranchIndex];
                    if (currentBranchDependents) {
                        for (let i = 0; i < currentBranchDependents.length; i++) {
                            const dependentIndex = currentBranchDependents[i];
                            if (dependentIndex < 0 || list.branches[dependentIndex]) {
                                const indexOf = dependents.indexOf(dependentIndex);
                                if (indexOf >= 0) {
                                    dependents.splice(indexOf, 1);
                                    if (indexOf <= currentIndex) {
                                        --currentIndex;
                                    }
                                }
                                dependents.push(dependentIndex);
                            }
                        }
                    }
                }
                ++currentIndex;
            }
            return dependents;
        }
        /**
         * Checks whether a branch item is completed.
         * @param item The item to check.
         * @returns Whether the specified branch item is completed.
         */
        isBranchItemCompleted(item) {
            var _a;
            if (item.isOverridden.get()) {
                return true;
            }
            else {
                let sufficientItemCompleted = false;
                let necessaryItemsCompleted = undefined;
                for (let i = 0; !(sufficientItemCompleted || !!necessaryItemsCompleted) && i < item.isBranchCompleted.length; i++) {
                    const dependencyBranchLogic = (_a = item.def.branchLogic[i]) !== null && _a !== void 0 ? _a : exports.ChecklistBranchItemLogicType.None;
                    if (dependencyBranchLogic !== exports.ChecklistBranchItemLogicType.None) {
                        if (dependencyBranchLogic === exports.ChecklistBranchItemLogicType.Sufficient) {
                            sufficientItemCompleted || (sufficientItemCompleted = item.isBranchCompleted[i].get());
                        }
                        else {
                            necessaryItemsCompleted !== null && necessaryItemsCompleted !== void 0 ? necessaryItemsCompleted : (necessaryItemsCompleted = true);
                            necessaryItemsCompleted && (necessaryItemsCompleted = item.isBranchCompleted[i].get());
                        }
                    }
                }
                return sufficientItemCompleted || !!necessaryItemsCompleted;
            }
        }
        /**
         * Destroys this provider. Once destroyed, this provider will no longer update its provided data, and can no longer
         * be paused or resumed.
         */
        destroy() {
            this.isAlive = false;
            for (const sub of this.subscriptions) {
                sub.destroy();
            }
        }
    }

    /**
     * A FADEC for turbojets. Controls engine throttle based on throttle lever position and other inputs.
     */
    class JetFadec {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param modes The modes supported by this FADEC, ordered from highest to lowest priority.
         * @param throttleInfos An array containing information pertaining to the throttles controlled by this FADEC. The
         * order of modes in the array determines their priority during mode selection. On every update cycle, the FADEC
         * iterates through the modes array in order, calling `accept()` on each mode until a value of `true` is returned.
         * Therefore, modes positioned earlier in the array have a higher priority for selection.
         * @param desiredThrottleMin The min value to limit the desiredThrottle by. Defaults to -100.
         * @param desiredThrottleMax The max value to limit the desiredThrottle by. Defaults to 100.
         */
        constructor(bus, modes, throttleInfos, desiredThrottleMin = -100, desiredThrottleMax = 100) {
            this.bus = bus;
            this.modes = modes;
            this.desiredThrottleMin = desiredThrottleMin;
            this.desiredThrottleMax = desiredThrottleMax;
            this.publisher = this.bus.getPublisher();
            this.updateHandler = this.update.bind(this);
            this.realTimeSub = ConsumerSubject.create(this.bus.getSubscriber().on('realTime'), 0);
            this.updateTimer = null;
            this.lastUpdateTime = 0;
            this.throttleInfos = throttleInfos.map(info => {
                return Object.assign(Object.assign({}, info), { throttleSimVar: `GENERAL ENG THROTTLE LEVER POSITION:${info.index}`, thrustSimVar: `TURB ENG JET THRUST:${info.index}`, n1SimVar: `TURB ENG N1:${info.index}`, correctedN1SimVar: `TURB ENG CORRECTED N1:${info.index}`, fadecModeTopic: `fadec_mode_${info.index}` });
            });
            this.lastModes = this.throttleInfos.map(() => null);
            const sub = this.bus.getSubscriber();
            this.throttleLeverPositionValues = throttleInfos.map(info => {
                return ConsumerValue.create(sub.on(info.leverPosTopic), 0);
            });
            this.engineStates = {};
            for (const throttle of throttleInfos) {
                this.engineStates[throttle.index] = {
                    throttleLeverPos: 0,
                    throttle: 0,
                    thrust: 0,
                    n1: 0,
                    n1Corrected: 0
                };
            }
        }
        /**
         * Turns this FADEC on. If this FADEC is already running, then it will be turned off before turning on again with
         * the specified frequency.
         * @param frequency The frequency, in hertz, at which this FADEC will update.
         */
        start(frequency) {
            this.stop();
            this.publisher.pub('fadec_active', true, true, true);
            this.updateTimer = setInterval(this.updateHandler, 1000 / frequency);
        }
        /**
         * Turns this FADEC off.
         */
        stop() {
            if (this.updateTimer === null) {
                return;
            }
            clearInterval(this.updateTimer);
            this.updateTimer = null;
            for (let i = 0; i < this.throttleInfos.length; i++) {
                this.setMode(i, null);
            }
            this.publisher.pub('fadec_active', false, true, true);
        }
        /**
         * Updates this FADEC.
         */
        update() {
            const realTime = Date.now();
            // Check if the current time has diverged from the event bus value by more than 1 second.
            // If it has, we are probably paused in the menu and should skip the update.
            if (realTime - this.realTimeSub.get() >= 1000) {
                return;
            }
            const dt = realTime - this.lastUpdateTime;
            this.onUpdate(dt);
            this.lastUpdateTime = realTime;
        }
        /**
         * A method called when this FADEC is updated.
         * @param dt The elapsed real time, in milliseconds, since the last update.
         */
        onUpdate(dt) {
            this.updateEngineStates();
            this.updateThrottles(dt);
        }
        /**
         * Updates the states for this FADEC's engines.
         */
        updateEngineStates() {
            for (let i = 0; i < this.throttleInfos.length; i++) {
                const info = this.throttleInfos[i];
                const state = this.engineStates[info.index];
                state.throttleLeverPos = this.throttleLeverPositionValues[i].get();
                state.throttle = SimVar.GetSimVarValue(info.throttleSimVar, exports.SimVarValueType.Percent) / 100;
                state.thrust = SimVar.GetSimVarValue(info.thrustSimVar, exports.SimVarValueType.Pounds);
                state.n1 = SimVar.GetSimVarValue(info.n1SimVar, exports.SimVarValueType.Percent);
                state.n1Corrected = SimVar.GetSimVarValue(info.correctedN1SimVar, exports.SimVarValueType.Percent);
            }
        }
        /**
         * Updates this FADEC's engine throttles.
         * @param dt The elapsed real time, in milliseconds, since the last update.
         */
        updateThrottles(dt) {
            for (let i = 0; i < this.throttleInfos.length; i++) {
                this.updateThrottle(i, dt);
            }
        }
        /**
         * Updates a throttle.
         * @param index The index of the throttle in this FADEC's throttle list.
         * @param dt The elapsed time, in milliseconds, since the last update.
         */
        updateThrottle(index, dt) {
            const info = this.throttleInfos[index];
            const { throttleLeverPos, throttle, thrust, n1, n1Corrected } = this.engineStates[info.index];
            // These values are expected to be changed in the for loop below
            let desiredThrottle = throttleLeverPos;
            let visibleThrottlePos = throttleLeverPos;
            for (let i = 0; i < this.modes.length; i++) {
                const mode = this.modes[i];
                if (mode.accept(info.index, throttleLeverPos, throttle, thrust, n1, n1Corrected)) {
                    this.setMode(index, mode);
                    desiredThrottle = mode.computeDesiredThrottle(info.index, throttleLeverPos, throttle, thrust, n1, n1Corrected, dt);
                    visibleThrottlePos = mode.getVisibleThrottlePos(info.index, throttleLeverPos);
                    break;
                }
            }
            const clampedThrottle = MathUtils.clamp(desiredThrottle * 100, this.desiredThrottleMin, this.desiredThrottleMax);
            const clampedVisibleThrottlePos = MathUtils.clamp(visibleThrottlePos, -1, 1);
            if (isFinite(clampedThrottle)) {
                SimVar.SetSimVarValue(info.throttleSimVar, exports.SimVarValueType.Percent, clampedThrottle);
            }
            if (isFinite(clampedVisibleThrottlePos)) {
                SimVar.SetSimVarValue(info.visiblePosSimVar, exports.SimVarValueType.Number, clampedVisibleThrottlePos);
            }
        }
        /**
         * Sets a FADEC mode for a throttle.
         * @param index The index of the throttle in this FADEC's throttle list.
         * @param mode The mode to set.
         */
        setMode(index, mode) {
            var _a;
            if (mode === this.lastModes[index]) {
                return;
            }
            this.lastModes[index] = mode;
            this.publisher.pub(this.throttleInfos[index].fadecModeTopic, (_a = mode === null || mode === void 0 ? void 0 : mode.name) !== null && _a !== void 0 ? _a : '', true, true);
        }
    }
    // This is pulled from the native sim code. Commonly used when correcting N1.
    JetFadec.MSFS_STANDARD_SEA_LEVEL_TEMP_RANKINE = 518.69;

    /**
     * A manager for virtual throttle levers. Intercepts key events that control engine throttle settings and uses them
     * to move virtual throttle levers instead. The positions of the virtual throttle levers are published on the event
     * bus.
     */
    class ThrottleLeverManager {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param onInitCallback A callback function to be executed once this manager is initialized.
         * @param options Options with which to configure the manager.
         */
        constructor(bus, onInitCallback, options) {
            var _a, _b;
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.keyEventHandlerController = {
                getThrottleLeverPos: this.getThrottleLeverPos.bind(this),
                setRawThrottleLeverPosition: this.setRawThrottleLeverPosition.bind(this),
                changeRawThrottleLeverPosition: this.changeRawThrottleLeverPosition.bind(this)
            };
            this.defaultKeyEventHandlers = new Map();
            this.rawStep = (_a = ((options === null || options === void 0 ? void 0 : options.step) !== undefined ? options.step * ThrottleLeverManager.RAW_MAX : options === null || options === void 0 ? void 0 : options.rawStep)) !== null && _a !== void 0 ? _a : ThrottleLeverManager.DEFAULT_RAW_STEP;
            this.rawStepSmall = (_b = ((options === null || options === void 0 ? void 0 : options.stepSmall) !== undefined ? options.stepSmall * ThrottleLeverManager.RAW_MAX : options === null || options === void 0 ? void 0 : options.rawStepSmall)) !== null && _b !== void 0 ? _b : ThrottleLeverManager.DEFAULT_RAW_STEP_SMALL;
            this.throttleLevers = ArrayUtils.create(ThrottleLeverManager.THROTTLE_COUNT, index => {
                return {
                    index: index + 1,
                    topic: `v_throttle_lever_pos_${index + 1}`,
                    rawPosition: 0
                };
            });
            this.throttleLeverHandler = options === null || options === void 0 ? void 0 : options.throttleLeverHandler;
            this.keyEventHandler = options === null || options === void 0 ? void 0 : options.keyEventHandler;
            this.initPromise = this.init(onInitCallback);
        }
        /**
         * Initializes this manager. Once initialized, this manager will intercept all throttle key events to move virtual
         * throttle levers and publish the positions of the virtual throttle levers to the event bus.
         * @param onInitCallback A callback function to be executed once this manager is initialized.
         */
        async init(onInitCallback) {
            this.keyEventManager = await KeyEventManager.getManager(this.bus);
            const sub = this.bus.getSubscriber();
            const virtualPositions = this.throttleLevers.map(lever => {
                return ConsumerValue.create(sub.on(lever.topic), NaN);
            });
            for (let i = 0; i < this.throttleLevers.length; i++) {
                // Initialize position to the pre-existing virtual lever position, if available. Otherwise, initialize to the
                // engine throttle lever position simvar.
                const lever = this.throttleLevers[i];
                const virtualPosition = virtualPositions[i].get();
                const initialPosition = isNaN(virtualPosition)
                    ? MathUtils.clamp(SimVar.GetSimVarValue(`GENERAL ENG THROTTLE LEVER POSITION:${lever.index}`, 'Percent') / 100, 0, 1)
                    : virtualPosition;
                virtualPositions[i].destroy();
                this.setRawThrottleLeverPosition(initialPosition * ThrottleLeverManager.RAW_MAX, i + 1);
            }
            // ---- SET EVENTS ----
            this.keyEventManager.interceptKey('AXIS_THROTTLE_SET', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE1_SET', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE2_SET', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE3_SET', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE4_SET', false);
            this.keyEventManager.interceptKey('THROTTLE_AXIS_SET_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_AXIS_SET_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_AXIS_SET_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_AXIS_SET_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_AXIS_SET_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE_SET', false);
            this.keyEventManager.interceptKey('THROTTLE1_SET', false);
            this.keyEventManager.interceptKey('THROTTLE2_SET', false);
            this.keyEventManager.interceptKey('THROTTLE3_SET', false);
            this.keyEventManager.interceptKey('THROTTLE4_SET', false);
            this.keyEventManager.interceptKey('THROTTLE_FULL', false);
            this.keyEventManager.interceptKey('THROTTLE1_FULL', false);
            this.keyEventManager.interceptKey('THROTTLE2_FULL', false);
            this.keyEventManager.interceptKey('THROTTLE3_FULL', false);
            this.keyEventManager.interceptKey('THROTTLE4_FULL', false);
            this.keyEventManager.interceptKey('THROTTLE_FULL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_FULL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_FULL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_FULL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_FULL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE_CUT', false);
            this.keyEventManager.interceptKey('THROTTLE1_CUT', false);
            this.keyEventManager.interceptKey('THROTTLE2_CUT', false);
            this.keyEventManager.interceptKey('THROTTLE3_CUT', false);
            this.keyEventManager.interceptKey('THROTTLE4_CUT', false);
            this.keyEventManager.interceptKey('THROTTLE_CUT_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_CUT_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_CUT_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_CUT_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_CUT_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE_10', false);
            this.keyEventManager.interceptKey('THROTTLE_20', false);
            this.keyEventManager.interceptKey('THROTTLE_30', false);
            this.keyEventManager.interceptKey('THROTTLE_40', false);
            this.keyEventManager.interceptKey('THROTTLE_50', false);
            this.keyEventManager.interceptKey('THROTTLE_60', false);
            this.keyEventManager.interceptKey('THROTTLE_70', false);
            this.keyEventManager.interceptKey('THROTTLE_80', false);
            this.keyEventManager.interceptKey('THROTTLE_90', false);
            this.keyEventManager.interceptKey('THROTTLE_DETENT_NEXT', false);
            this.keyEventManager.interceptKey('THROTTLE_DETENT_PREV', false);
            // ---- INC EVENTS ----
            this.keyEventManager.interceptKey('INCREASE_THROTTLE', false);
            this.keyEventManager.interceptKey('THROTTLE_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE_RANGE_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE1_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE2_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE3_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE4_INCR', false);
            this.keyEventManager.interceptKey('THROTTLE_INCREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_INCREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_INCREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_INCREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_INCREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE_INCR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE1_INCR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE2_INCR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE3_INCR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE4_INCR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE_INCREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_INCREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_INCREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_INCREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_INCREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE_PLUS', false);
            // ---- DEC EVENTS ----
            this.keyEventManager.interceptKey('DECREASE_THROTTLE', false);
            this.keyEventManager.interceptKey('THROTTLE_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE_RANGE_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE1_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE2_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE3_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE4_DECR', false);
            this.keyEventManager.interceptKey('THROTTLE_DECREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_DECREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_DECREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_DECREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_DECREASE_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE_DECR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE1_DECR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE2_DECR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE3_DECR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE4_DECR_SMALL', false);
            this.keyEventManager.interceptKey('THROTTLE_DECREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE1_DECREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE2_DECREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE3_DECREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('THROTTLE4_DECREASE_SMALL_EX1', false);
            this.keyEventManager.interceptKey('AXIS_THROTTLE_MINUS', false);
            this.initDefaultKeyEventHandlers();
            sub.on('key_intercept').handle(this.onKeyIntercepted.bind(this));
            onInitCallback && onInitCallback();
        }
        /**
         * Initializes this manager's default key event handlers.
         */
        initDefaultKeyEventHandlers() {
            const axisSet = (index, data) => {
                if (data.value0 !== undefined) {
                    this.setRawThrottleLeverPosition((data.value0 + ThrottleLeverManager.RAW_MAX) / 2, index, data.key);
                }
            };
            const set = (index, data) => {
                if (data.value0 !== undefined) {
                    this.setRawThrottleLeverPosition(data.value0, index, data.key);
                }
            };
            const setConstant = (position, index, data) => {
                this.setRawThrottleLeverPosition(position, index, data.key);
            };
            const changeConstant = (delta, index, data) => {
                this.changeRawThrottleLeverPosition(delta, index, data.key);
            };
            const change = (direction, index, data) => {
                if (data.value0 !== undefined) {
                    this.changeRawThrottleLeverPosition(data.value0 * direction, index, data.key);
                }
            };
            const axisSetAll = axisSet.bind(this, undefined);
            this.defaultKeyEventHandlers.set('AXIS_THROTTLE_SET', axisSetAll);
            this.defaultKeyEventHandlers.set('THROTTLE_AXIS_SET_EX1', axisSetAll);
            this.defaultKeyEventHandlers.set('THROTTLE_SET', set.bind(this, undefined));
            const setFullAll = setConstant.bind(this, ThrottleLeverManager.RAW_MAX, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_FULL', setFullAll);
            this.defaultKeyEventHandlers.set('THROTTLE_FULL_EX1', setFullAll);
            const setZeroAll = setConstant.bind(this, 0, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_CUT', setZeroAll);
            this.defaultKeyEventHandlers.set('THROTTLE_CUT_EX1', setZeroAll);
            this.defaultKeyEventHandlers.set('THROTTLE_10', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.1, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_20', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.2, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_30', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.3, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_40', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.4, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_50', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.5, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_60', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.6, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_70', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.7, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_80', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.8, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_90', setConstant.bind(this, ThrottleLeverManager.RAW_MAX * 0.9, undefined));
            this.defaultKeyEventHandlers.set('THROTTLE_DETENT_NEXT', setFullAll);
            this.defaultKeyEventHandlers.set('THROTTLE_DETENT_PREV', setZeroAll);
            const incrAll = changeConstant.bind(this, this.rawStep, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_INCR', incrAll);
            this.defaultKeyEventHandlers.set('INCREASE_THROTTLE', incrAll);
            this.defaultKeyEventHandlers.set('THROTTLE_RANGE_INCR', incrAll);
            this.defaultKeyEventHandlers.set('THROTTLE_INCREASE_EX1', incrAll);
            const incrSmallAll = changeConstant.bind(this, this.rawStepSmall, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_INCR_SMALL', incrSmallAll);
            this.defaultKeyEventHandlers.set('THROTTLE_INCREASE_SMALL_EX1', incrSmallAll);
            this.defaultKeyEventHandlers.set('AXIS_THROTTLE_PLUS', change.bind(this, 1, undefined));
            const decrAll = changeConstant.bind(this, -this.rawStep, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_DECR', decrAll);
            this.defaultKeyEventHandlers.set('DECREASE_THROTTLE', decrAll);
            this.defaultKeyEventHandlers.set('THROTTLE_RANGE_DECR', decrAll);
            this.defaultKeyEventHandlers.set('THROTTLE_DECREASE_EX1', decrAll);
            const decrSmallAll = changeConstant.bind(this, -this.rawStepSmall, undefined);
            this.defaultKeyEventHandlers.set('THROTTLE_DECR_SMALL', decrSmallAll);
            this.defaultKeyEventHandlers.set('THROTTLE_DECREASE_SMALL_EX1', decrSmallAll);
            this.defaultKeyEventHandlers.set('AXIS_THROTTLE_MINUS', change.bind(this, -1, undefined));
            for (let i = 1; i <= 4; i++) {
                const axisSetSingle = axisSet.bind(this, i);
                this.defaultKeyEventHandlers.set(`AXIS_THROTTLE${i}_SET`, axisSetSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_AXIS_SET_EX1`, axisSetSingle);
                const setSingle = set.bind(this, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_SET`, setSingle);
                const setFullSingle = setConstant.bind(this, ThrottleLeverManager.RAW_MAX, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_FULL`, setFullSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_FULL_EX1`, setFullSingle);
                const setZeroSingle = setConstant.bind(this, 0, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_CUT`, setZeroSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_CUT_EX1`, setZeroSingle);
                const incrSingle = changeConstant.bind(this, this.rawStep, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_INCR`, incrSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_INCREASE_EX1`, incrSingle);
                const incrSmallSingle = changeConstant.bind(this, this.rawStepSmall, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_INCR_SMALL`, incrSmallSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_INCREASE_SMALL_EX1`, incrSmallSingle);
                const decrSingle = changeConstant.bind(this, -this.rawStep, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_DECR`, decrSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_DECREASE_EX1`, decrSingle);
                const decrSmallSingle = changeConstant.bind(this, -this.rawStepSmall, i);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_DECR_SMALL`, decrSmallSingle);
                this.defaultKeyEventHandlers.set(`THROTTLE${i}_DECREASE_SMALL_EX1`, decrSmallSingle);
            }
        }
        /**
         * Waits until this manager has been initialized.
         * @returns A Promise that is fulfilled when this manager has been initialized.
         */
        awaitInit() {
            return this.initPromise;
        }
        /**
         * Sets the position of a throttle lever.
         * @param index The index of the throttle lever to set, from 1 to 4, inclusive.
         * @param pos The position to set, in the range -1 to +1.
         * @returns The throttle lever position at the end of the operation, in the range -1 to +1.
         * @throws Error if `index` is out of bounds.
         */
        setThrottleLeverPos(index, pos) {
            return this.setThrottleLeverPosRaw(index, pos * ThrottleLeverManager.RAW_MAX) / ThrottleLeverManager.RAW_MAX;
        }
        /**
         * Gets the position of a throttle lever.
         * @param index The index of the throttle lever to get, from 1 to 4, inclusive.
         * @returns The throttle lever position, in the range -1 to +1.
         */
        getThrottleLeverPos(index) {
            return this.getThrottleLeverPosRaw(index) / ThrottleLeverManager.RAW_MAX;
        }
        /**
         * Changes the position of a throttle lever.
         * @param index The index of the throttle lever to change, from 1 to 4, inclusive.
         * @param delta The amount by which to change the lever position. The full lever range is expressed as -1 to +1.
         * @returns The throttle lever position at the end of the operation, in the range -1 to +1.
         * @throws Error if `index` is out of bounds.
         */
        changeThrottleLeverPos(index, delta) {
            return this.changeThrottleLeverPosRaw(index, delta * ThrottleLeverManager.RAW_MAX) / ThrottleLeverManager.RAW_MAX;
        }
        /**
         * Sets the raw position of a throttle lever.
         * @param index The index of the throttle lever to set, from 1 to 4, inclusive.
         * @param pos The raw position to set, in the range -16384 to +16384.
         * @returns The raw throttle lever position at the end of the operation, in the range -16384 to +16384.
         * @throws Error if `index` is out of bounds.
         */
        setThrottleLeverPosRaw(index, pos) {
            if (index < 1 || index > ThrottleLeverManager.THROTTLE_COUNT) {
                throw new Error(`ThrottleLeverManager: throttle index (${index}) out of bounds`);
            }
            this.setRawThrottleLeverPosition(pos, index);
            return this.throttleLevers[index - 1].rawPosition;
        }
        /**
         * Gets the raw position of a throttle lever.
         * @param index The index of the throttle lever to get, from 1 to 4, inclusive.
         * @returns The raw throttle lever position, in the range -16384 to +16384.
         * @throws Error if `index` is out of bounds.
         */
        getThrottleLeverPosRaw(index) {
            if (index < 1 || index > ThrottleLeverManager.THROTTLE_COUNT) {
                throw new Error(`ThrottleLeverManager: throttle index (${index}) out of bounds`);
            }
            return this.throttleLevers[index - 1].rawPosition;
        }
        /**
         * Changes the raw position of a throttle lever.
         * @param index The index of the throttle lever to change, from 1 to 4, inclusive.
         * @param delta The amount by which to change the raw lever position. The full lever range is expressed as -16384 to
         * +16384.
         * @returns The raw throttle lever position at the end of the operation, in the range -16384 to +16384.
         * @throws Error if `index` is out of bounds.
         */
        changeThrottleLeverPosRaw(index, delta) {
            if (index < 1 || index > ThrottleLeverManager.THROTTLE_COUNT) {
                throw new Error(`ThrottleLeverManager: throttle index (${index}) out of bounds`);
            }
            this.changeRawThrottleLeverPosition(delta, index);
            return this.throttleLevers[index - 1].rawPosition;
        }
        /**
         * Responds to key intercept events.
         * @param data The event data.
         */
        onKeyIntercepted(data) {
            const defaultHandler = this.defaultKeyEventHandlers.get(data.key);
            if (defaultHandler) {
                (this.keyEventHandler && this.keyEventHandler(data, this.keyEventHandlerController)) || defaultHandler(data);
            }
        }
        /**
         * Sets a raw throttle lever position.
         * @param rawPosition The raw position to set.
         * @param index The index of the throttle lever to set. If undefined, the positions of all throttle levers will be
         * set.
         * @param keyEvent The key event responsible for this change, or `undefined` if this change was not triggered by a
         * key event.
         */
        setRawThrottleLeverPosition(rawPosition, index, keyEvent) {
            rawPosition = MathUtils.clamp(Math.round(rawPosition), -ThrottleLeverManager.RAW_MAX, ThrottleLeverManager.RAW_MAX);
            const end = (index !== null && index !== void 0 ? index : ThrottleLeverManager.THROTTLE_COUNT) + 1;
            for (let i = index !== null && index !== void 0 ? index : 1; i < end; i++) {
                const lever = this.throttleLevers[i - 1];
                if (this.throttleLeverHandler) {
                    rawPosition = MathUtils.clamp(Math.round(this.throttleLeverHandler(lever.index, lever.rawPosition / ThrottleLeverManager.RAW_MAX, rawPosition / ThrottleLeverManager.RAW_MAX, keyEvent) * ThrottleLeverManager.RAW_MAX), -ThrottleLeverManager.RAW_MAX, ThrottleLeverManager.RAW_MAX);
                }
                if (rawPosition !== lever.rawPosition) {
                    lever.rawPosition = rawPosition;
                    this.publishThrottleLeverPosition(lever);
                }
            }
        }
        /**
         * Changes a raw throttle lever position.
         * @param delta The amount by which to change the raw lever position.
         * @param index The index of the throttle lever to change. If undefined, the positions of all throttle levers will be
         * changed.
         * @param keyEvent The key event responsible for this change, or `undefined` if this change was not triggered by a
         * key event.
         */
        changeRawThrottleLeverPosition(delta, index, keyEvent) {
            const end = (index !== null && index !== void 0 ? index : ThrottleLeverManager.THROTTLE_COUNT) + 1;
            for (let i = index !== null && index !== void 0 ? index : 1; i < end; i++) {
                this.setRawThrottleLeverPosition(this.throttleLevers[i - 1].rawPosition + delta, i, keyEvent);
            }
        }
        /**
         * Publishes a virtual throttle lever position to the event bus.
         * @param lever The lever whose position to publish.
         */
        publishThrottleLeverPosition(lever) {
            this.publisher.pub(lever.topic, lever.rawPosition / ThrottleLeverManager.RAW_MAX, true, true);
        }
    }
    ThrottleLeverManager.THROTTLE_COUNT = 4;
    ThrottleLeverManager.RAW_MAX = 16384;
    ThrottleLeverManager.DEFAULT_RAW_STEP = 256;
    ThrottleLeverManager.DEFAULT_RAW_STEP_SMALL = 128;

    /**
     * A FADEC for turboprops. Controls engine throttle based on throttle lever position and other inputs.
     */
    class TurbopropFadec {
        /**
         * Constructor.
         * @param bus The event bus.
         * @param modes The modes supported by this FADEC, ordered from highest to lowest priority.
         * @param throttleInfos An array containing information pertaining to the throttles controlled by this FADEC. The
         * order of modes in the array determines their priority during mode selection. On every update cycle, the FADEC
         * iterates through the modes array in order, calling `accept()` on each mode until a value of `true` is returned.
         * Therefore, modes positioned earlier in the array have a higher priority for selection.
         * @param desiredThrottleMin The min value to limit the desiredThrottle by. Defaults to -100.
         * @param desiredThrottleMax The max value to limit the desiredThrottle by. Defaults to 100.
         */
        constructor(bus, modes, throttleInfos, desiredThrottleMin = -100, desiredThrottleMax = 100) {
            this.bus = bus;
            this.modes = modes;
            this.desiredThrottleMin = desiredThrottleMin;
            this.desiredThrottleMax = desiredThrottleMax;
            this.publisher = this.bus.getPublisher();
            this.updateHandler = this.update.bind(this);
            this.realTimeSub = ConsumerSubject.create(this.bus.getSubscriber().on('realTime'), 0);
            this.updateTimer = null;
            this.lastUpdateTime = 0;
            this.throttleInfos = throttleInfos.map(info => {
                return Object.assign(Object.assign({}, info), { throttleSimVar: `GENERAL ENG THROTTLE LEVER POSITION:${info.index}`, torqueSimVar: `TURB ENG MAX TORQUE PERCENT:${info.index}`, ngSimVar: `TURB ENG N1:${info.index}`, correctedNgSimVar: `TURB ENG CORRECTED N1:${info.index}`, fadecModeTopic: `fadec_mode_${info.index}` });
            });
            this.lastModes = this.throttleInfos.map(() => null);
            const sub = this.bus.getSubscriber();
            this.throttleLeverPositionValues = throttleInfos.map(info => {
                return ConsumerValue.create(sub.on(info.leverPosTopic), 0);
            });
            this.engineStates = {};
            for (const throttle of throttleInfos) {
                this.engineStates[throttle.index] = {
                    throttleLeverPos: 0,
                    throttle: 0,
                    torque: 0,
                    ng: 0,
                    ngCorrected: 0
                };
            }
        }
        /**
         * Turns this FADEC on. If this FADEC is already running, then it will be turned off before turning on again with
         * the specified frequency.
         * @param frequency The frequency, in hertz, at which this FADEC will update.
         */
        start(frequency) {
            this.stop();
            this.publisher.pub('fadec_active', true, true, true);
            this.updateTimer = setInterval(this.updateHandler, 1000 / frequency);
        }
        /**
         * Turns this FADEC off.
         */
        stop() {
            if (this.updateTimer === null) {
                return;
            }
            clearInterval(this.updateTimer);
            this.updateTimer = null;
            for (let i = 0; i < this.throttleInfos.length; i++) {
                this.setMode(i, null);
            }
            this.publisher.pub('fadec_active', false, true, true);
        }
        /**
         * Updates this FADEC.
         */
        update() {
            const realTime = Date.now();
            // Check if the current time has diverged from the event bus value by more than 1 second.
            // If it has, we are probably paused in the menu and should skip the update.
            if (realTime - this.realTimeSub.get() >= 1000) {
                return;
            }
            const dt = realTime - this.lastUpdateTime;
            this.onUpdate(dt);
            this.lastUpdateTime = realTime;
        }
        /**
         * A method called when this FADEC is updated.
         * @param dt The elapsed real time, in milliseconds, since the last update.
         */
        onUpdate(dt) {
            this.updateEngineStates();
            this.updateThrottles(dt);
        }
        /**
         * Updates the states for this FADEC's engines.
         */
        updateEngineStates() {
            for (let i = 0; i < this.throttleInfos.length; i++) {
                const info = this.throttleInfos[i];
                const state = this.engineStates[info.index];
                state.throttleLeverPos = this.throttleLeverPositionValues[i].get();
                state.throttle = SimVar.GetSimVarValue(info.throttleSimVar, exports.SimVarValueType.Percent) / 100;
                state.torque = SimVar.GetSimVarValue(info.torqueSimVar, exports.SimVarValueType.PercentOver100);
                state.ng = SimVar.GetSimVarValue(info.ngSimVar, exports.SimVarValueType.Percent);
                state.ngCorrected = SimVar.GetSimVarValue(info.correctedNgSimVar, exports.SimVarValueType.Percent);
            }
        }
        /**
         * Updates this FADEC's engine throttles.
         * @param dt The elapsed real time, in milliseconds, since the last update.
         */
        updateThrottles(dt) {
            for (let i = 0; i < this.throttleInfos.length; i++) {
                this.updateThrottle(i, dt);
            }
        }
        /**
         * Updates a throttle.
         * @param index The index of the throttle in this FADEC's throttle list.
         * @param dt The elapsed time, in milliseconds, since the last update.
         */
        updateThrottle(index, dt) {
            const info = this.throttleInfos[index];
            const { throttleLeverPos, throttle, torque: thrust, ng, ngCorrected } = this.engineStates[info.index];
            // These values are expected to be changed in the for loop below
            let desiredThrottle = throttleLeverPos;
            let visibleThrottlePos = throttleLeverPos;
            for (let i = 0; i < this.modes.length; i++) {
                const mode = this.modes[i];
                if (mode.accept(info.index, throttleLeverPos, throttle, thrust, ng, ngCorrected)) {
                    this.setMode(index, mode);
                    desiredThrottle = mode.computeDesiredThrottle(info.index, throttleLeverPos, throttle, thrust, ng, ngCorrected, dt);
                    visibleThrottlePos = mode.getVisibleThrottlePos(info.index, throttleLeverPos);
                    break;
                }
            }
            const clampedThrottle = MathUtils.clamp(desiredThrottle * 100, this.desiredThrottleMin, this.desiredThrottleMax);
            const clampedVisibleThrottlePos = MathUtils.clamp(visibleThrottlePos, -1, 1);
            if (isFinite(clampedThrottle)) {
                SimVar.SetSimVarValue(info.throttleSimVar, exports.SimVarValueType.Percent, clampedThrottle);
            }
            if (isFinite(clampedVisibleThrottlePos)) {
                SimVar.SetSimVarValue(info.visiblePosSimVar, exports.SimVarValueType.Number, clampedVisibleThrottlePos);
            }
        }
        /**
         * Sets a FADEC mode for a throttle.
         * @param index The index of the throttle in this FADEC's throttle list.
         * @param mode The mode to set.
         */
        setMode(index, mode) {
            var _a;
            if (mode === this.lastModes[index]) {
                return;
            }
            this.lastModes[index] = mode;
            this.publisher.pub(this.throttleInfos[index].fadecModeTopic, (_a = mode === null || mode === void 0 ? void 0 : mode.name) !== null && _a !== void 0 ? _a : '', true, true);
        }
    }
    // This is pulled from the native sim code. Commonly used when correcting ng.
    TurbopropFadec.MSFS_STANDARD_SEA_LEVEL_TEMP_RANKINE = 518.69;

    /**
     * A class for defining FMC components
     *
     * ## LSK events
     *
     * This class hierarchy deals with LSK presses in the following order:
     *
     * 1. {@link handleSelectKey} - this is called by the page when it receives an LSk associated with the component
     * 2. IF the DELETE flag is set - call {@link FmcComponentOptions.onDelete} if it's present + apply return value logic - otherwise continue
     * 3. ELSE - call {@link FmcComponentOptions.onSelected} if it's present and apply return value logic - continue otherwise
     * 4. call {@link onHandleSelectKey} - overridden by a subclass
     */
    class FmcComponent {
        /** @inheritDoc */
        constructor(page, options) {
            this.page = page;
            this.options = options;
            this.isDisabled = this.options.disabled !== undefined && typeof this.options.disabled === 'function' ? this.options.disabled : () => !!this.options.disabled;
        }
        /**
         * Invalidates the component and queues a re-render if one is not already queued
         */
        invalidate() {
            this.page.invalidate();
        }
        /** @inheritDoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async handleSelectKey(event) {
            var _a, _b;
            if (this.isDisabled()) {
                return false;
            }
            if (event.isDelete) {
                if (this.options.onDelete) {
                    const result = await this.options.onDelete();
                    if (result === true || typeof result === 'string') {
                        if ((_a = this.options.clearScratchpadOnSelectedHandled) !== null && _a !== void 0 ? _a : true) {
                            this.page.screen.clearScratchpad();
                        }
                        return result;
                    }
                }
            }
            if (this.options.onSelected) {
                try {
                    const result = await this.options.onSelected(event.scratchpadContents);
                    if (result === true || typeof result === 'string') {
                        if ((_b = this.options.clearScratchpadOnSelectedHandled) !== null && _b !== void 0 ? _b : true) {
                            this.page.screen.clearScratchpad();
                        }
                        return result;
                    }
                }
                catch (error) {
                    return Promise.reject(error);
                }
            }
            return this.onHandleSelectKey(event);
        }
        /**
         * Returns the component's options
         * @returns The options.
         */
        getOptions() { return this.options; }
    }

    /**
     * An {@link FmcComponent} for displaying values according to formats
     */
    class DisplayField extends FmcComponent {
        /**
         * Gets the raw value of this display field
         * @returns the value
         */
        get rawValue() {
            return this.value;
        }
        /** @inheritDoc */
        constructor(page, options) {
            super(page, options);
            this.options = options;
            this.value = null;
        }
        /**
         * Creates and registers a binding on the page, linking this field with a subscribable
         * @param subscribable the subscribable to bind to
         * @returns the created binding (usually not needed)
         */
        bind(subscribable) {
            this.page.addBinding(subscribable.sub((value) => this.takeValue(value, true), true));
            return this;
        }
        /**
         * Takes an input value, displays it and stores it
         * @param value the new input value
         * @param shouldInvalidate whether or not to invalidate the page
         */
        takeValue(value, shouldInvalidate = false) {
            this.value = value;
            if (shouldInvalidate) {
                this.invalidate();
            }
        }
        /** @inheritDoc */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async onHandleSelectKey(event) {
            return false;
        }
        /** @inheritDoc */
        render() {
            var _a, _b, _c;
            let renderOutput;
            if (typeof this.options.formatter === 'object') {
                if (this.value !== null) {
                    renderOutput = this.options.formatter.format(this.value);
                }
                else {
                    renderOutput = (_a = this.options.formatter.nullValueString) !== null && _a !== void 0 ? _a : '';
                }
            }
            else {
                renderOutput = this.options.formatter(this.value);
            }
            if (typeof renderOutput !== 'string') {
                return renderOutput;
            }
            const styleStr = (typeof this.options.style === 'function') ? this.options.style(this.value) : this.options.style;
            return `${(_b = this.options.prefix) !== null && _b !== void 0 ? _b : ''}${renderOutput}${(_c = this.options.suffix) !== null && _c !== void 0 ? _c : ''}${styleStr !== null && styleStr !== void 0 ? styleStr : ''}`;
        }
    }

    /**
     * A structure representing a source of named data that can be modified
     */
    class DataInterface {
        /**
         * Constructs a new `DataInterface`
         *
         * @param input  an input for data
         * @param modify a callback when the data needs to be modified
         */
        constructor(input, modify) {
            this.input = input;
            this.modify = modify;
        }
        /**
         * Creates a {@link DataInterface} from a {@link MutableSubscribable}
         * @param sub the {@link MutableSubscribable} to bind to
         * @returns the {@link DataInterface}
         */
        static fromMutSubscribable(sub) {
            return new DataInterface(sub, (value) => sub.set(value));
        }
        /**
         * Creates a {@link DataInterface} from a {@link Consumer}
         * @param consumer the {@link Consumer} to bind to (get)
         * @param modifier the modifier to use when the value is modified (set)
         * @returns the {@link DataInterface}
         */
        static fromConsumer(consumer, modifier) {
            return new DataInterface(consumer, (value) => modifier(value));
        }
    }

    /**
     * An {@link FmcComponent} for displaying and accepting new values according to a validator and formatter.
     *
     * ## deleteAllowed
     * This class deals with LSK presses that have the DELETE flag active using a default implementation
     * of {@link FmcComponentOptions.onDelete}, which checks `options.deleteAllowed` - if `true` or not set,the `onValidLskInput` subject
     * is set to `null` - if `false`, the "INVALID DELETE" scratchpad message is returned and handled by {@link FmcComponent.handleSelectKey}.
     *
     * ## onModified
     * This class also introduces another LSK handler, {@link EditableFieldOptions.onModified}, which is run after a value has been
     * validated (it is not called for invalid values) and applies return value logic. This runs after the flow
     * described for {@link FmcComponent}.
     */
    class EditableField extends DisplayField {
        /** @inheritDoc */
        constructor(page, options) {
            super(page, options);
            this.page = page;
            this.options = options;
            this.valueChanged = new SubEvent();
        }
        /**
         * Binds the input field to a mutable subscribable.
         * @param subscribable the mutable subscribable
         * @returns the instance of this {@link EditableField}
         */
        bind(subscribable) {
            if (SubscribableUtils.isMutableSubscribable(subscribable)) {
                this.bindSource(DataInterface.fromMutSubscribable(subscribable));
            }
            else {
                super.bind(subscribable);
            }
            return this;
        }
        /**
         * Binds the input field to a Consumer.
         * @param consumer the consumer to bind to (get)
         * @param modifier the modifier to use when the value is modified (set)
         * @returns the instance of this {@link EditableField}
         */
        bindConsumer(consumer, modifier) {
            return this.bindSource(DataInterface.fromConsumer(consumer, modifier));
        }
        /**
         * Binds the input field to a data interface.
         * @param source the data interface
         * @returns the instance of this {@link EditableField}
         */
        bindSource(source) {
            if (SubscribableUtils.isSubscribable(source.input)) {
                this.page.addBinding(source.input.sub((value) => this.takeValue(value, true), true));
            }
            else {
                this.page.addBinding(source.input.handle((value) => this.takeValue(value, true)));
            }
            this.page.addBinding(this.valueChanged.on((sender, data) => source.modify(data)));
            return this;
        }
    }

    /**
     * {@link Validator} for parsing raw string values
     */
    const RawValidator = {
        /** @inheritDoc */
        parse(input) {
            return input !== null && input !== void 0 ? input : '';
        },
    };
    /**
     * {@link Formatter} for displaying raw string values
     */
    const RawFormatter = {
        nullValueString: '',
        /** @inheritDoc */
        format(value) {
            if (typeof value === 'number') {
                return value.toString();
            }
            return value !== null && value !== void 0 ? value : '';
        },
    };

    /**
     * A field for displaying a link to navigate to another page
     */
    class PageLinkField extends DisplayField {
        /** @inheritDoc */
        constructor(page, options) {
            var _a;
            const opts = {
                formatter: RawFormatter,
                style: options.disabled ? '[disabled]' : '',
                disabled: options.disabled,
                clearScratchpadOnSelectedHandled: false,
                onSelected: (_a = options.onSelected) !== null && _a !== void 0 ? _a : (async () => { page.screen.navigateTo(options.route, options.params); return true; }),
            };
            super(page, opts);
            this.takeValue(options.label);
        }
        /**
         * Creates an {@link PageLinkField}
         * @param page    the parent {@link FmcPage}
         * @param label  the label to display
         * @param route the route to navigate to (will disable link when empty)
         * @param disabled whether the link is disabled
         * @param params Parameters for the route
         * @returns the {@link PageLinkField}
         */
        static createLink(page, label, route, disabled = false, params) {
            if (route === '') {
                disabled = true;
            }
            return new PageLinkField(page, { label, route, disabled, params });
        }
    }

    /**
     * A field for displaying a switch label.
     */
    class SwitchLabel extends EditableField {
        /** @inheritDoc */
        constructor(page, options = {}) {
            super(page, Object.assign({
                formatter: RawFormatter,
                activeStyle: 'green',
                caret: 'none',
                separator: 'slash',
            }, options));
            this.page = page;
        }
        /** @inheritDoc */
        render() {
            let string = '';
            if (this.value !== null) {
                for (let i = 0; i < this.options.optionStrings.length; i++) {
                    const choice = this.options.optionStrings[i];
                    string += choice;
                    if (i === this.value) {
                        string += `[${this.options.activeStyle} d-text]`;
                    }
                    else {
                        string += '[s-text]';
                    }
                    if (i !== this.options.optionStrings.length - 1) {
                        switch (this.options.separator) {
                            case 'slash':
                                string += '/[d-text]';
                                break;
                            case 'arrows':
                                // if (i === this.value - 1) {
                                //   string += `←→[${this.options.activeStyle} d-text]`;
                                // } else {
                                string += '←→[d-text]';
                                // }
                                break;
                        }
                    }
                }
                switch (this.options.caret) {
                    case 'left':
                        string = `<[]${string}`;
                        break;
                    case 'right':
                        string = `${string}>[]`;
                        break;
                }
            }
            return string;
        }
        /** @inheritDoc */
        async onHandleSelectKey() {
            if (this.value === null) {
                this.value = 0;
            }
            else {
                this.value = (this.value + 1) % this.options.optionStrings.length;
            }
            this.valueChanged.notify(this, this.value);
            this.invalidate();
            return true;
        }
    }

    /**
     * An {@link FmcComponent} for displaying and accepting new values according to a validator and formatter.
     *
     * ## deleteAllowed
     *
     * This class deals with LSK presses that have the DELETE flag active using a default implementation
     * of {@link FmcComponentOptions.onDelete}, which checks `options.deleteAllowed` - if `true` or not set,the `onValidLskInput` subject
     * is set to `null` - if `false`, the "INVALID DELETE" scratchpad message is returned and handled by {@link FmcComponent.handleSelectKey}.
     *
     * ## onModified
     *
     * This class also introduces another LSK handler, {@link InputFieldOptions.onModified}, which is run after a value has been
     * validated (it is not called for invalid values) and applies return value logic. This runs after the flow
     * described for {@link FmcComponent}.
     */
    class TextInputField extends EditableField {
        /** @inheritDoc */
        constructor(page, options) {
            super(page, options);
            this.options = options;
            // Default `onDelete` behaviour for input fields
            if (this.options.onDelete === undefined) {
                this.options.onDelete = async () => {
                    var _a;
                    if ((_a = this.options.deleteAllowed) !== null && _a !== void 0 ? _a : true) {
                        // We cannot check at runtime if the field can take null, so we kinda have to do that
                        this.valueChanged.notify(this, null);
                        // Always clear s-pad since it has DELETE in it
                        this.page.screen.clearScratchpad();
                        return true;
                    }
                    else {
                        return Promise.reject(this.page.screen.options.textInputFieldDisallowedDeleteThrowValue);
                    }
                };
            }
        }
        /**
         * Allows text input to be programmatically sent to the field.
         *
         * @param input the text input
         * @returns a Promise that resolves to a boolean or string
         */
        async takeTextInput(input) {
            return this.handleTextInputInternal(input);
        }
        /** @inheritDoc */
        async onHandleSelectKey(event) {
            var _a;
            if (event.isDelete) {
                if ((_a = this.options.deleteAllowed) !== null && _a !== void 0 ? _a : true) {
                    this.valueChanged.notify(this, null); // We cannot check at runtime if the field can take null, so we kinda have to do that
                    // Always clear s-pad since it has DELETE in it
                    this.page.screen.clearScratchpad();
                    return true;
                }
                else {
                    return Promise.reject(this.page.screen.options.textInputFieldDisallowedDeleteThrowValue);
                }
            }
            return this.handleTextInputInternal(event.scratchpadContents);
        }
        /**
         * Internal handling of text input
         *
         * @param text the input text
         * @returns a Promise that resolves to a boolean or string
         */
        async handleTextInputInternal(text) {
            var _a, _b, _c;
            const parsedValue = await this.options.formatter.parse(text);
            if (parsedValue !== null) {
                // Process an `onModified` hook if we have one
                if (this.options.onModified) {
                    const onModifiedResult = await this.options.onModified(parsedValue);
                    // If the hook returns `true`, we return its value but also run the side effect.
                    // An error returned would throw an exception, so this would not run.
                    if (onModifiedResult === true || typeof onModifiedResult === 'string') {
                        if ((_a = this.options.clearScratchpadOnValueAccepted) !== null && _a !== void 0 ? _a : true) {
                            this.page.screen.clearScratchpad();
                        }
                    }
                    else {
                        this.valueChanged.notify(this, parsedValue);
                    }
                    if (onModifiedResult === false) {
                        // Here, `false` means that the `onModified` hook did not handle the value - not that the LSK is inactive
                        // so we return `true` and clear the scratchpad instead, as we presume the hook did what it wanted
                        if ((_b = this.options.clearScratchpadOnValueAccepted) !== null && _b !== void 0 ? _b : true) {
                            this.page.screen.clearScratchpad();
                        }
                        return true;
                    }
                    return onModifiedResult;
                }
                else {
                    this.valueChanged.notify(this, parsedValue);
                    if ((_c = this.options.clearScratchpadOnValueAccepted) !== null && _c !== void 0 ? _c : true) {
                        this.page.screen.clearScratchpad();
                    }
                    return true;
                }
            }
            else {
                return Promise.reject(this.page.screen.options.textInputFieldParseFailThrowValue);
            }
        }
    }

    /**
     * A field for displaying a toggle label.
     */
    class ToggleLabel extends EditableField {
        /** @inheritDoc */
        constructor(page, options = {}) {
            const opts = Object.assign({
                formatter: RawFormatter,
                activeStyle: 'green',
            }, options);
            super(page, opts);
        }
        /** @inheritDoc */
        render() {
            var _a;
            const value = this.value;
            return `${value && this.options.text[1] !== undefined ? this.options.text[1] : this.options.text[0]}[${value ? `${(_a = this.options.activeStyle) !== null && _a !== void 0 ? _a : 'green'} d-text` : 's-text'}]`;
        }
        /** @inheritDoc */
        async onHandleSelectKey() {
            this.value = !this.value;
            this.valueChanged.notify(this, this.value);
            this.invalidate();
            return true;
        }
    }

    /**
     * Represents possible lifetimes for FmcPages
     */
    exports.FmcPageLifecyclePolicy = void 0;
    (function (FmcPageLifecyclePolicy) {
        /**
         * Page is only created and initialized once, the first time it is navigated to, the reloaded and resumed.
         */
        FmcPageLifecyclePolicy[FmcPageLifecyclePolicy["Singleton"] = 0] = "Singleton";
        /**
         * Page is re-created and re-initialized every time it is navigated to.
         */
        FmcPageLifecyclePolicy[FmcPageLifecyclePolicy["Transient"] = 1] = "Transient";
    })(exports.FmcPageLifecyclePolicy || (exports.FmcPageLifecyclePolicy = {}));
    /**
     * Base abstract class for FMC pages
     */
    class AbstractFmcPage {
        /**
         * Obtains the current value of the page's props
         *
         * @throws if the props have not yet been initialised
         *
         * @returns the props value
         */
        get props() {
            if (!this._props) {
                throw new Error('Props not yet initialized; make sure to access props only after a page has been first instantiated');
            }
            return this._props;
        }
        /**
         * Ctor
         * @param bus the event bus
         * @param screen the FMC screen instance
         * @param props the initial props for this page
         */
        constructor(bus, screen, props) {
            this.bus = bus;
            this.screen = screen;
            this.memorizedComponents = [];
            this.bindings = [];
            this.pageExtensions = [];
            this.params = new Map();
            this.isDirty = false;
            /**
             * Handles when update/refresh of the FMCPage based on clock input
             * @param d is the Clock Value from the Event Bus
             */
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            this.clockHandler = (d) => {
                if (this.isDirty) {
                    this.isDirty = false;
                    this.initialRender();
                }
            };
            this.isInitialized = false;
            this.currentOutput = [];
            this.screen = screen;
            this.clockConsumer = this.bus.getSubscriber().on('realTime').atFrequency(10, false);
            this._props = props;
            this.clockSub = this.clockConsumer.handle(this.clockHandler, true);
        }
        /**
         * Initializes the page.
         *
         * Use this for setting up subscriptions and such.
         */
        init() {
            var _a;
            this.addBinding(this.screen.currentSubpageIndex.sub(() => this.invalidate()));
            for (const extension of this.pageExtensions) {
                (_a = extension.onPageInit) === null || _a === void 0 ? void 0 : _a.call(extension);
            }
            this.onInit();
        }
        /**
         * Init lifecycle hook
         */
        onInit() {
            // Do nothing
        }
        /**
         * Pauses the page and calls appropriate event handlers
         */
        pause() {
            var _a;
            for (const binding of this.bindings) {
                if (!binding.isAlive) {
                    continue;
                }
                binding.pause();
            }
            this.isDirty = false;
            this.clockSub.pause();
            this.onPause();
            for (const extension of this.pageExtensions) {
                (_a = extension.onPagePause) === null || _a === void 0 ? void 0 : _a.call(extension);
            }
        }
        /**
         * Pause lifecycle hook
         */
        onPause() {
            // Do nothing
        }
        /**
         * Resumes the page and calls appropriate event handlers
         *
         * @param props the props to pass in to the page, if applicable
         */
        resume(props) {
            var _a;
            if (props !== undefined) {
                this._props = props;
            }
            for (const binding of this.bindings) {
                if (!binding.isAlive) {
                    continue;
                }
                binding.resume(true);
            }
            this.onResume();
            for (const extension of this.pageExtensions) {
                (_a = extension.onPageResume) === null || _a === void 0 ? void 0 : _a.call(extension);
            }
            this.isDirty = true;
            this.clockSub.resume(true);
        }
        /**
         * Resume lifecycle hook
         */
        onResume() {
            // Do nothing
        }
        /** Is called when the page button for the current page is pressed while already on that page. */
        onPageButtonPressed() {
            // Do nothing
        }
        /**
         * Destroys the page and calls appropriate event handlers
         */
        destroy() {
            var _a;
            this.isDirty = false;
            this.clockSub.destroy();
            for (const binding of this.bindings) {
                if (!binding.isAlive) {
                    continue;
                }
                binding.destroy();
            }
            this.onDestroy();
            for (const extension of this.pageExtensions) {
                (_a = extension.onPageDestroyed) === null || _a === void 0 ? void 0 : _a.call(extension);
            }
        }
        /**
         * Destroy lifecycle hook
         */
        onDestroy() {
            // Do nothing
        }
        /**
         * Invalidates the render and sets the component into the dirty state
         */
        invalidate() {
            this.isDirty = true;
        }
        /**
         * Initial render function
         * @throws If a `PositionedFmcColumn` attempts to return an `FmcRenderTemplate` from its render function (only `string`s are allowed).
         */
        initialRender() {
            var _a, _b;
            if (!this.isInitialized) {
                return;
            }
            const templates = this.render();
            for (const extension of this.pageExtensions) {
                (_a = extension.onPageRendered) === null || _a === void 0 ? void 0 : _a.call(extension, templates);
            }
            this.screen.currentSubpageCount.set(templates.length);
            const template = templates[this.screen.currentSubpageIndex.get() - 1];
            const render = [];
            this.memorizedComponents.length = 0;
            for (let i = 0; i < template.length; i++) {
                if (!render[i]) {
                    render[i] = [];
                }
                const row = template[i];
                const renderRow = render[i];
                for (let j = 0; j < row.length; j++) {
                    const col = row[j];
                    if (col instanceof FmcComponent) {
                        if (!this.memorizedComponents[i]) {
                            this.memorizedComponents[i] = [null, null, null];
                        }
                        this.memorizedComponents[i][j] = col;
                        const componentRender = col.render();
                        if (Array.isArray(componentRender)) {
                            for (let k = 0; k < componentRender.length; k++) {
                                const componentRenderRow = componentRender[k];
                                for (let l = 0; l < componentRenderRow.length; l++) {
                                    if (!render[i + k]) {
                                        render[i + k] = [];
                                    }
                                    if (!this.memorizedComponents[i + k]) {
                                        this.memorizedComponents[i + k] = [null, null, null];
                                    }
                                    (componentRenderRow[l] !== '') && (render[i + k][j + l] = componentRenderRow[l]);
                                    (componentRenderRow[l] !== '') && (this.memorizedComponents[i + k][j] = col);
                                }
                            }
                        }
                        else if (componentRender !== '' || renderRow[j] === undefined) {
                            renderRow[j] = componentRender;
                        }
                    }
                    else if (typeof col === 'string' && col !== '' && renderRow[j] === undefined) {
                        renderRow[j] = col;
                    }
                    else if (AbstractFmcPage.isPositionedFmcColumn(col)) {
                        const content = col[0];
                        const text = typeof content === 'string' ? content : content.render();
                        if (typeof text !== 'string') {
                            throw new Error('FmcPage: PositionedFmcColumns must return a string value.');
                        }
                        renderRow[j] = {
                            text,
                            columnIndex: col[1],
                            alignment: (_b = col[2]) !== null && _b !== void 0 ? _b : 'left',
                        };
                    }
                }
            }
            this.currentOutput = render;
            this.renderCallback(this.currentOutput, template, 0);
        }
        /**
         * Accepts a page extension
         *
         * @param extension the page extension
         */
        acceptPageExtension(extension) {
            this.pageExtensions.push(extension);
        }
        /**
         * Tests whether an `FmcRenderTemplateColumn` is a `PositionedFmcColumn`.
         * @param column The `FmcRenderTemplateColumn` to test.
         * @returns Whether the column is a `PositionedFmcColumn`.
         */
        static isPositionedFmcColumn(column) {
            if (!Array.isArray(column)) {
                return false;
            }
            const firstElementPasses = typeof column[0] === 'string' || column[0] instanceof FmcComponent;
            const secondElementPasses = typeof column[1] === 'number';
            const thirdElementPasses = typeof column[2] === 'string' || column[2] === undefined;
            if (typeof column[2] === 'string' && column[2] !== 'left' && column[2] !== 'right') {
                return false;
            }
            return firstElementPasses && secondElementPasses && thirdElementPasses;
        }
        /**
         * Tests whether the input is a `RenderedPositionedFmcColumn`.
         * @param column The input to test.
         * @returns Whether the column is a `RenderedPositionedFmcColumn`.
         */
        static isRenderedPositionedFmcColumn(column) {
            return column !== undefined && typeof column !== 'string' && column.text !== undefined && column.columnIndex !== undefined;
        }
        /**
         * Registers a subscription or a ConsumerSubject on the page. This enables the page to manage the lifecycle of all
         * subscriptions that are used within it, for example by pausing them whenever the page is out of view, or destroying
         * them when the page is destroyed.
         *
         * @param binding a subscription
         */
        addBinding(binding) {
            this.bindings.push(binding);
        }
        /**
         * Handles a line select key received by the FMC, before passing it on to components
         *
         * @param event the LSK event
         * @returns a Promise that resolves to a boolean or string
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async handleLineSelectKey(event) {
            var _a, _b;
            for (const extension of this.pageExtensions) {
                const processedByExtension = (_a = extension.onPageHandleSelectKey) === null || _a === void 0 ? void 0 : _a.call(extension, event);
                if (processedByExtension) {
                    return true;
                }
            }
            const componentAtSk = (_b = this.memorizedComponents[event.row]) === null || _b === void 0 ? void 0 : _b[event.col];
            if (componentAtSk) {
                const selectKeyHandled = await componentAtSk.handleSelectKey(event);
                if (selectKeyHandled !== false) {
                    return selectKeyHandled;
                }
            }
            return await this.onHandleSelectKey(event);
        }
        /**
         * Handles a line select key received by the FMC, before passing it on to components
         *
         * @param event the LSK event
         * @returns a Promise that resolves to a boolean or string
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async onHandleSelectKey(event) {
            // Do nothing
            return false;
        }
        /**
         * Handles a scrolling event received by the FMC, before passing it on to components
         *
         * @param event the scrolling event
         * @returns a Promise that resolves to a boolean or string
         */
        async handleScrolling(event) {
            var _a;
            for (const extension of this.pageExtensions) {
                const processedByExtension = (_a = extension.onPageHandleScrolling) === null || _a === void 0 ? void 0 : _a.call(extension, event);
                if (processedByExtension) {
                    return true;
                }
            }
            return this.onHandleScrolling(event);
        }
        /**
         * Handles a scrolling event received by the FMC, before passing it on to components
         *
         * @param event the scrolling event
         * @returns a Promise that resolves to a boolean or string
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async onHandleScrolling(event) {
            // Do nothing
            return false;
        }
    }
    /**
     * Configures the {@link FmcPageLifecyclePolicy} for this page
     */
    AbstractFmcPage.lifecyclePolicy = exports.FmcPageLifecyclePolicy.Singleton;
    /**
     * Utilities for the {@link PageConstructor} type
     */
    class PageConstructorUtils {
        /** Ctor */
        constructor() {
        }
        /**
         * Returns whether a value is a `PageConstructor`
         *
         * @param input the value to test
         *
         * @returns whether the value conforms to the type
         */
        static isPageConstructor(input) {
            return !!input && typeof input === 'object' && 'lifecyclePolicy' in input;
        }
    }

    /**
     * Utility class to drive list
     */
    class FmcListUtility {
        /**
         * FMC List Utility Class
         * @param page                  The Fmc Page
         * @param data                  The row input data
         * @param renderRow             Function to call when the list needs to be re-rendered with new data
         * @param size                  row count of the list per page
         * @param firstPageSize         row count of the first page of the list
         */
        constructor(page, data, renderRow, size = 5, firstPageSize = undefined) {
            this.page = page;
            this.data = data;
            this.renderRow = renderRow;
            this.size = size;
            this.firstPageSize = firstPageSize;
        }
        /**
         * Returns a rendered list page for a specified page
         * @param page The page number to render
         * @returns The FmcRenderTemplate
         */
        renderList(page) {
            const rows = [];
            let startIndex = (page - 1) * this.size;
            if (this.firstPageSize !== undefined && page > 1) {
                startIndex = this.firstPageSize + ((page - 2) * this.size);
            }
            for (let i = startIndex; i < startIndex + this.size; i++) {
                const prevData = this.data.tryGet(i - 1);
                const data = this.data.tryGet(i);
                const nextData = this.data.tryGet(i + 1);
                const subRows = this.renderRow(this.page, i - startIndex, prevData, data, nextData);
                subRows.forEach(row => rows.push(row));
            }
            return rows;
        }
        /**
         * Handles when the Select Key is pressed.
         * @param event The Select Key Event.
         * @returns Whether the event was handled by this component.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async handleSelectKey(event) {
            return false;
        }
    }

    /**
     * FMC Page factory, used to instantiate pages.
     *
     * FMC Pages should be instantiated with the required data and objects (FMS instances, avionics-specific utilities, event busses)
     * in instances of subclasses of this type.
     */
    class FmcPageFactory {
    }

    /**
     * A router for an FMC screen.
     *
     * This registers routes and handles setting the appropriate page and params.
     */
    class FmcRouter {
        constructor() {
            this.routes = {};
            this.routesDefaultProps = new Map();
            this.currentRoute = Subject.create('/');
            this.currentSubpageIndex = Subject.create(1);
            this.currentSubpageCount = Subject.create(1);
        }
        /**
         * Adds a route to the router
         *
         * @param route the route string
         * @param page the target page constructor
         * @param defaultProps the default props to pass in to the page
         */
        addRoute(route, page, defaultProps) {
            this.routes[route] = page;
            this.routesDefaultProps.set(page, defaultProps);
        }
        /**
         * Gets the associated page (or undefined) for a route
         *
         * @param routeString the route string
         *
         * @returns the associated page
         */
        getPageForRoute(routeString) {
            return this.routes[routeString.split('#', 2)[0].trim()];
        }
        /**
         * Gets the associated route (or undefined) for a page
         *
         * @param pageCtor the page constructor
         *
         * @returns the associated route
         */
        getRouteForPage(pageCtor) {
            for (const [route, ctor] of Object.entries(this.routes)) {
                if (ctor === pageCtor) {
                    return route;
                }
            }
            return undefined;
        }
        /**
         * Gets the associated subpage index (after the hash) or 1 by default
         *
         * @param routeString the route string
         *
         * @returns the associated subpage index
         */
        getSubpageForRoute(routeString) {
            var _a;
            return parseInt((_a = routeString.split('#', 2)[1]) !== null && _a !== void 0 ? _a : '1');
        }
        /**
         * Returns the default props for a given page class
         *
         * @param page the page class
         *
         * @throws if the page was not registered
         *
         * @returns the default props
         */
        getDefaultPropsForPage(page) {
            if (!this.routesDefaultProps.has(page)) {
                throw new Error('No default props registered for page');
            }
            return this.routesDefaultProps.get(page);
        }
        /**
         * Moves to the previous subpage if there is one available
         *
         * @returns whether or not the subpage was changed
         */
        prevSubpage() {
            const currentIndex = this.currentSubpageIndex.get();
            if (currentIndex === 1) {
                this.currentSubpageIndex.set(this.currentSubpageCount.get());
            }
            else {
                this.currentSubpageIndex.set(currentIndex - 1);
            }
            return true;
        }
        /**
         * Moves to the next subpage if there is one available
         *
         * @returns whether or not the subpage was changed
         */
        nextSubpage() {
            const currentIndex = this.currentSubpageIndex.get();
            if (currentIndex >= this.currentSubpageCount.get()) {
                this.currentSubpageIndex.set(1);
            }
            else {
                this.currentSubpageIndex.set(currentIndex + 1);
            }
            return true;
        }
        /**
         * Moves to the specified subpage if there is one available
         *
         * @param index Desired subpage index (1-based)
         *
         * @returns whether or not the subpage was changed
         */
        setSubpage(index) {
            const currentIndex = this.currentSubpageIndex.get();
            if (index < 1 || index > this.currentSubpageCount.get() || currentIndex === index) {
                return false;
            }
            this.currentSubpageIndex.set(index);
            return true;
        }
    }

    /**
     * Scratchpad for an FMC screen
     */
    class FmcScratchpad {
        /**
         * Ctor
         * @param bus the event bus
         * @param options the options
         * @param renderCallback the render callback
         */
        constructor(bus, options, renderCallback) {
            this.bus = bus;
            this.renderCallback = renderCallback;
            this.options = {
                renderRow: -1,
                cellWidth: 16,
                style: 'white d-text',
                deleteText: 'DELETE',
                surroundingText: ['', ''],
                errorTextCentered: false,
                clearScratchpadOnError: false,
            };
            this.contents = Subject.create('');
            this.errorContents = Subject.create('');
            this.renderedText = Subject.create('');
            this.isInDelete = Subject.create(false);
            Object.assign(this.options, options);
            MappedSubject.create(this.contents, this.errorContents, this.isInDelete).sub(() => this.renderText(), true);
        }
        /**
         * Appends a string on the scratchpad
         * @param char the string to append
         */
        typeContents(char) {
            this.isInDelete.set(false);
            this.clearError();
            const contents = this.contents.get();
            this.contents.set(`${contents}${char}`);
        }
        /**
         * Deletes the last character on the scratchpad
         */
        backspace() {
            this.clearError();
            const contents = this.contents.get();
            this.contents.set(contents.substring(0, Math.max(0, contents.length - 1)));
        }
        /**
         * Clears the scratchpad
         *
         * @param clearError whether to clear any scratchpad error content, defaults to `true`
         */
        clear(clearError = true) {
            if (clearError) {
                this.clearError();
            }
            this.delete(false, false);
            this.contents.set('');
        }
        /**
         * Clears the scratchpad error
         */
        clearError() {
            this.errorContents.set('');
        }
        /**
         * Sets the scratchpad to an error message
         * @param error The error message
         */
        error(error) {
            if (this.options.clearScratchpadOnError) {
                this.isInDelete.set(false);
                this.contents.set('');
            }
            this.errorContents.set(error);
        }
        /**
         * Sets the scratchpad in DELETE mode (or not)
         *
         * @param value optional value to force, otherwise the value is set to `true`
         * @param clearError whether to clear any scratchpad error content, defaults to `true`
         */
        delete(value, clearError = true) {
            if (clearError) {
                this.clearError();
            }
            this.isInDelete.set(value !== null && value !== void 0 ? value : !this.isInDelete.get());
        }
        /**
         * Renders the scratchpad and sets the subject
         */
        renderText() {
            const leftCleanSurroundText = this.options.surroundingText[0]
                .replace(/__LSB/g, '[')
                .replace(/__RSB/g, ']')
                .replace(/\[[\w\s]+]/g, '');
            const rightCleanSurroundText = this.options.surroundingText[1]
                .replace(/__LSB/g, '[')
                .replace(/__RSB/g, ']')
                .replace(/\[[\w\s]+]/g, '');
            const surroundingTextWidth = leftCleanSurroundText.length + rightCleanSurroundText.length;
            const spaceToPadTo = this.options.cellWidth - surroundingTextWidth;
            // We use `\u00a0` instead of the normal space character here, due to what seems to be an issue involving the regular space character
            // causing a lack of repaints in some situations.
            let renderText;
            if (this.errorContents.get()) {
                const errorContents = this.errorContents.get();
                if (this.options.errorTextCentered) {
                    const leftPad = '\u00a0'.repeat(Math.floor((spaceToPadTo - errorContents.length) / 2));
                    const rightPad = '\u00a0'.repeat(Math.ceil((spaceToPadTo - errorContents.length) / 2));
                    renderText = `${leftPad}${errorContents}${rightPad}`;
                }
                else {
                    renderText = errorContents.padEnd(spaceToPadTo, '\u00a0');
                }
            }
            else if (this.isInDelete.get()) {
                renderText = this.options.deleteText.padEnd(spaceToPadTo, '\u00a0');
            }
            else {
                renderText = this.contents.get().padEnd(spaceToPadTo, '\u00a0');
            }
            const leftText = this.options.surroundingText[0];
            const rightText = this.options.surroundingText[1];
            const styleString = this.options.style ? `[${this.options.style}]` : '';
            this.renderedText.set(`${leftText}${renderText}${styleString}${rightText}`);
        }
    }

    /**
     * An FMC screen.
     *
     * This is the main object used to encapsulate an FMC screen and its rendering.
     *
     * Type parameter `T` should be a subclass of the {@link AbstractFmcPage} that is universally used by pages in this implementation.
     * This is done so that different avionics can have different types for different kinds of events and any data that pages might need to be
     * instantiated with.
     */
    class FmcScreen {
        /**
         * Ctor
         * @param bus the event bus
         * @param pageFactory the page factory to be used for creating FMC pages for this screen
         * @param options the screen options
         * @param renderer the renderer to use for this screen
         * @param scratchpad the scratchpad to use for this screen
         */
        constructor(bus, pageFactory, options, renderer, scratchpad) {
            this.bus = bus;
            this.pageFactory = pageFactory;
            this.renderer = renderer;
            this.scratchpad = scratchpad;
            this.router = new FmcRouter();
            this.pageInstanceCache = new Map();
            this.options = {
                screenDimensions: {
                    cellWidth: 24,
                    cellHeight: 12,
                },
                lskNotHandledThrowValue: 'KEY NOT ACTIVE',
                textInputFieldDisallowedDeleteThrowValue: 'INVALID DELETE',
                textInputFieldParseFailThrowValue: 'INVALID ENTRY',
                enableScratchpad: true,
                eventPrefix: '',
            };
            this.currentlyDisplayedPage = null;
            this.pendingPageAdditions = {};
            this.pendingPageReplacements = {};
            this.attachedPageExtensions = [];
            Object.assign(this.options, options);
            if (this.options.enableScratchpad) {
                this.scratchpad.renderedText.sub((text) => {
                    this.renderer.editOutputTemplate([[text]], this.scratchpadRenderRow, []);
                });
            }
        }
        /**
         * Processes page replacement calls made by plugins. Must be called after plugins are initialized, and before stock routes are added.
         */
        processPluginPageAdditions() {
            for (const [route, [pageCtor, routeEvent, initialTypedParameters]] of Object.entries(this.pendingPageAdditions)) {
                this.addPageRoute(route, pageCtor, routeEvent, initialTypedParameters);
            }
        }
        /**
         * Processes page replacement calls made by plugins. Must be called after plugins are initialized, and after stock routes are added.
         */
        processPluginPageReplacements() {
            for (const [route, [pageCtor, routeEvent, initialTypedParameters]] of Object.entries(this.pendingPageReplacements)) {
                this.addPageRoute(route, pageCtor, routeEvent, initialTypedParameters);
            }
        }
        /**
         * Gets the current route
         *
         * @returns a string subject
         */
        get currentRoute() {
            return this.router.currentRoute;
        }
        /**
         * Gets the current subpage index for the current displayed page, 1-indexed
         *
         * @returns a number
         */
        get currentSubpageIndex() {
            return this.router.currentSubpageIndex;
        }
        /**
         * Gets the current number of subpages for the current displayed page
         *
         * @returns a number
         */
        get currentSubpageCount() {
            return this.router.currentSubpageCount;
        }
        /**
         * Navigates to a given route and displays the associated page, if available
         *
         * @param arg0 the route
         * @param arg1 the parameters to pass to the page
         *
         * @throws if a page class is passed in as the first argument and no associated route is found
         */
        navigateTo(arg0, arg1) {
            const pageCtor = typeof arg0 === 'string' ? this.router.getPageForRoute(arg0) : arg0;
            const pageRoute = typeof arg0 === 'string' ? arg0 : this.router.getRouteForPage(arg0);
            if (pageCtor === undefined || pageRoute === undefined) {
                throw new Error(`Could not find a page route for ${arg0}`);
            }
            this.router.currentRoute.set(pageRoute);
            let instance;
            if (pageCtor.lifecyclePolicy === exports.FmcPageLifecyclePolicy.Singleton) {
                const existingInstance = this.pageInstanceCache.get(pageCtor);
                instance = existingInstance !== null && existingInstance !== void 0 ? existingInstance : this.instantiatePage(pageCtor);
                this.pageInstanceCache.set(pageCtor, instance);
            }
            else {
                instance = this.instantiatePage(pageCtor);
            }
            if (this.currentlyDisplayedPage) {
                this.currentlyDisplayedPage.pause();
            }
            this.currentlyDisplayedPage = instance;
            instance.params.clear();
            if (arg1 && !PageConstructorUtils.isPageConstructor(arg1)) {
                const params = arg1;
                for (const key of Object.keys(params)) {
                    instance.params.set(key, params[key]);
                }
            }
            if (!instance.isInitialized) {
                instance.init();
            }
            this.router.currentSubpageCount.set(instance.render().length);
            this.router.setSubpage(this.router.getSubpageForRoute(pageRoute));
            instance.isInitialized = true;
            instance.resume(arg1);
        }
        /**
         * Clears the scratchpad, if applicable
         */
        clearScratchpad() {
            this.scratchpad.clear();
        }
        /**
         * Declares a route for a page class
         *
         * @param route the route to set
         * @param page the page to associate with it
         * @param routeEvent the event to associate with it
         * @param defaultProps default typed parameters to pass in to the page
         */
        addPageRoute(route, page, routeEvent, defaultProps) {
            this.router.addRoute(route, page, defaultProps !== null && defaultProps !== void 0 ? defaultProps : null);
            if (routeEvent) {
                this.onPrefixedEvent(routeEvent).handle(() => {
                    var _a;
                    if (this.currentRoute.get() === route) {
                        (_a = this.currentlyDisplayedPage) === null || _a === void 0 ? void 0 : _a.onPageButtonPressed();
                    }
                    else {
                        this.navigateTo(page, this.router.getDefaultPropsForPage(page));
                    }
                });
            }
        }
        /** @inheritDoc */
        addPluginPageRoute(route, page, routeEvent, defaultTypedParameters) {
            this.pendingPageAdditions[route] = [page, routeEvent, defaultTypedParameters];
        }
        /** @inheritDoc */
        replacePageRoute(route, page, routeEvent, defaultTypedParameters) {
            this.pendingPageReplacements[route] = [page, routeEvent, defaultTypedParameters];
        }
        /** @inheritDoc */
        attachPageExtension(pageClass, extensionCtor) {
            this.attachedPageExtensions.push([pageClass, extensionCtor]);
        }
        /**
         * Adds a list of LSK events bound to certain positions on the rendering grid
         *
         * @param array the LSK events and their positions
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        addLskEvents(array) {
            for (const lsk of array) {
                this.bus.getSubscriber().on(`${this.options.eventPrefix}${lsk[0]}`).handle(() => {
                    this.handleLineSelectKey(lsk[1], lsk[2]);
                });
            }
        }
        /**
         * Adds paging events bound the page slewing
         *
         * @param events the events to bind
         */
        addPagingEvents(events) {
            const subs = this.bus.getSubscriber();
            events.pageLeft && subs.on(`${this.options.eventPrefix}${events.pageLeft}`).handle(() => this.handlePagingKey('pageLeft'));
            events.pageRight && subs.on(`${this.options.eventPrefix}${events.pageRight}`).handle(() => this.handlePagingKey('pageRight'));
            events.pageUp && subs.on(`${this.options.eventPrefix}${events.pageUp}`).handle(() => this.handlePagingKey('pageUp'));
            events.pageDown && subs.on(`${this.options.eventPrefix}${events.pageDown}`).handle(() => this.handlePagingKey('pageDown'));
        }
        /**
         * Returns a consumer for an event prefixed for this particular screen.
         * @param event The event to subscribe to.
         * @returns A consumer for an event prefixed for this particular screen.
         * */
        onPrefixedEvent(event) {
            return this.bus.getSubscriber().on(`${this.options.eventPrefix}${event}`);
        }
        /**
         * Edits part of the screen output
         *
         * @param rowIndex the row index to insert at
         * @param output the output to insert
         *
         * @throws if `rowIndex` is too high
         */
        editOutputTemplate(rowIndex, output) {
            this.renderer.editOutputTemplate(output, rowIndex, []);
        }
        /**
         * Instantiates a page for this screen
         *
         * @param page the page constructor
         *
         * @returns the created page
         */
        instantiatePage(page) {
            const initialProps = this.router.getDefaultPropsForPage(page);
            const pageInstance = this.pageFactory.createPage(page, this.bus, this, initialProps, this.acceptPageOutput.bind(this));
            for (const [pageTarget, extension] of this.attachedPageExtensions) {
                if (pageInstance instanceof pageTarget) {
                    pageInstance.acceptPageExtension(new extension(pageInstance));
                }
            }
            return pageInstance;
        }
        /**
         * Method called when a page is rendered to the screen. This can be overridden to intercept the page data and act upon it.
         *
         * @param output the output template, displayed on the screen
         * @param template the render template, rendered by the page
         * @param atRowIndex the row index at which the render occurred
         */
        acceptPageOutput(output, template, atRowIndex) {
            const rows = [...output];
            const totalRows = this.options.screenDimensions.cellHeight - (this.options.enableScratchpad ? 2 : 0);
            for (let i = atRowIndex + rows.length; i < totalRows; i++) {
                rows.push(['']);
            }
            this.renderer.editOutputTemplate(rows, atRowIndex, template);
            if (this.options.enableScratchpad) {
                this.renderer.editOutputTemplate([[this.scratchpad.renderedText.get()]], this.scratchpadRenderRow, template);
            }
        }
        /**
         * Returns the row the scratchpad should render at
         *
         * @returns a number
         */
        get scratchpadRenderRow() {
            return this.scratchpad.options.renderRow === -1
                ? this.options.screenDimensions.cellHeight - 1
                : this.scratchpad.options.renderRow;
        }
        /**
         * Handles a line select key recieved by the FMC, before passing it on to pages
         *
         * @param row the row of the LSK
         * @param col the side of the lSK (0 or 1)
         */
        handleLineSelectKey(row, col) {
            if (this.currentlyDisplayedPage) {
                const event = {
                    row, col, scratchpadContents: this.scratchpad.contents.get(), isDelete: this.scratchpad.isInDelete.get(),
                };
                this.currentlyDisplayedPage.handleLineSelectKey(event).then((returnValue) => {
                    if (typeof returnValue === 'string') {
                        this.scratchpad.contents.set(returnValue);
                    }
                    else if (!returnValue) {
                        this.onLineSelectKeyUnhandled(event);
                    }
                }).catch((error) => this.onLineSelectKeyError(error));
            }
        }
        /**
         * Called when an LSK event is not handled by any code.
         *
         * @param selectKeyEvent the LSK event
         *
         * @throws the value of `options.lskNotHandledThrowValue` by default
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onLineSelectKeyUnhandled(selectKeyEvent) {
            throw this.options.lskNotHandledThrowValue;
        }
        /**
         * Called when an LSK error is thrown. Sets `this.scratchpad.errorContents` (if error is a string; otherwise throws) by default.
         *
         * @param error the LSK error
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onLineSelectKeyError(error) {
            if (typeof error === 'string') {
                this.scratchpad.errorContents.set(error);
            }
            else {
                console.error(`Unhandled error in LSK handler: ${error}`);
            }
        }
        /**
         * Handles a paging key recieved by the FMC
         *
         * @param event the paging key event
         */
        async handlePagingKey(event) {
            var _a;
            const handledByPage = await ((_a = this.currentlyDisplayedPage) === null || _a === void 0 ? void 0 : _a.handleScrolling(event));
            if (handledByPage) {
                return;
            }
            if (event === 'pageLeft') {
                this.router.prevSubpage();
            }
            if (event === 'pageRight') {
                this.router.nextSubpage();
            }
        }
    }

    /**
     * Abstract FmcPageExtension
     */
    class AbstractFmcPageExtension {
        /**
         * Constructor
         *
         * @param page the page
         */
        constructor(page) {
            this.page = page;
        }
    }

    /** FMC Renderer class */
    class SimpleFmcRenderer {
        /**
         * Ctor
         * @param eventBus The event bus.
         * @param targetElement The target element of the renderer.
         * @param options The options for the renderer.
         */
        constructor(eventBus, targetElement, options) {
            var _a;
            this.targetElement = targetElement;
            this.options = options;
            this.currentOutput = [];
            this.columnData = [];
            this.prevColumnData = [];
            this.hasChanges = false;
            this.colElArr = [];
            // HINT: Looks like thats too much but perf is better caching the text nodes
            this.colElNodeArr = [];
            this.rowElArr = [];
            this.containerTemplate = this.createContainerTemplate();
            this.columnData = this.createColumnInformation();
            this.prevColumnData = this.createColumnInformation();
            this.containerRef = this.targetElement;
            this.initializeContainer();
            this.clockConsumer = eventBus.getSubscriber().on('realTime').atFrequency((_a = this.options.updateFrequency) !== null && _a !== void 0 ? _a : 10, false);
            this.clockConsumer.handle(this.onClockUpdate.bind(this));
        }
        /** Initializes the container we will render to. */
        initializeContainer() {
            const container = this.containerTemplate.cloneNode(true);
            this.containerRef.replaceWith(container);
            // this.elecContainerRef.replaceChild(container, this.containerRef);
            this.containerRef = container;
            // build colElArr
            for (let r = 0; r < this.options.screenCellHeight; r++) {
                const rowEl = this.containerRef.childNodes[r];
                this.rowElArr.push(rowEl);
                const colArr = [];
                const colNodeArr = [];
                for (let c = 0; c < this.options.screenCellWidth; c++) {
                    const colEl = rowEl.childNodes[c + 1];
                    colArr.push(colEl);
                    colNodeArr.push(colEl.firstChild);
                }
                this.colElArr.push(colArr);
                this.colElNodeArr.push(colNodeArr);
            }
        }
        /** Handles the clock update event. */
        onClockUpdate() {
            if (this.hasChanges) {
                this.hasChanges = false;
                this.renderToDom();
            }
        }
        /** @inheritDoc */
        editOutputTemplate(output, rowIndex) {
            const rowsAvailable = (this.options.screenCellHeight) - rowIndex;
            if (rowsAvailable <= 0 || rowsAvailable < output.length) {
                throw new Error(`[SimpleFmcRenderer](editTemplate) Tried to write ${output.length - rowsAvailable} too many rows.`);
            }
            for (let i = rowIndex, c = 0; i < rowIndex + rowsAvailable && output[c]; i++, c++) {
                if (this.currentOutput[i] !== output[c]) {
                    this.currentOutput[i] = output[c];
                    this.hasChanges = true;
                }
            }
        }
        /** Renders the current template */
        renderToDom() {
            // get a new column data structure
            this.columnData = this.createColumnInformation();
            // parse and fill in the column data
            for (let index = 0; index < this.options.screenCellHeight; index++) {
                this.currentOutput[index] && this.buildRowInfo(this.currentOutput[index], index);
            }
            // go through all rows and columns and update the content if necessary
            for (let r = 0; r < this.options.screenCellHeight; r++) {
                for (let c = 0; c < this.options.screenCellWidth; c++) {
                    const colData = this.columnData[r][c];
                    const prevColData = this.prevColumnData[r][c];
                    if (colData.content !== prevColData.content || colData.styles !== prevColData.styles) {
                        if (colData.content !== prevColData.content) {
                            const colNodeEl = this.colElNodeArr[r][c];
                            colNodeEl.nodeValue = colData.content;
                        }
                        if (colData.styles !== prevColData.styles) {
                            const colEl = this.colElArr[r][c];
                            colEl.className = `fmc-letter ${colData.styles}`;
                        }
                    }
                }
            }
            this.prevColumnData = this.columnData;
        }
        /**
         * Parse row templates and build the column information.
         * @param template the template to parse
         * @param rowIndex the row index
         * @throws If something other than a `PositionedFmcColumn` follows another `PositionedFmcColumn`.
         */
        buildRowInfo(template, rowIndex) {
            const illegalOrder = template.some((column, index) => AbstractFmcPage.isRenderedPositionedFmcColumn(column) &&
                (!AbstractFmcPage.isRenderedPositionedFmcColumn(template[index + 1]) && template[index + 1] !== undefined));
            if (illegalOrder) {
                console.warn(template);
                throw new Error('FmcRenderer: Nothing may follow a `PositionedFmcColumn` in an `FmcRenderTemplateRow` except for another `PositionedFmcColumn`.');
            }
            // only content
            if (rowIndex < this.options.screenCellHeight && template) {
                if (template[0] && typeof template[0] === 'string' && template[0] !== '') {
                    // LEFT
                    this.buildColumnInformation(template[0], rowIndex, 'left');
                }
                if (template[1] && typeof template[1] === 'string' && template[1] !== '') {
                    // RIGHT
                    this.buildColumnInformation(template[1], rowIndex, 'right');
                }
                if (template[2] && typeof template[2] === 'string' && template[2] !== '') {
                    // CENTER
                    this.buildColumnInformation(template[2], rowIndex, 'center');
                }
            }
            template.forEach(column => {
                if (AbstractFmcPage.isRenderedPositionedFmcColumn(column) && column.text !== '') {
                    this.buildColumnInformation(column.text, rowIndex, column.alignment, column.columnIndex);
                }
            });
        }
        /**
         * Builds the data struct for the row's columns.
         * @param templateRowColumn template
         * @param rowIndex the row index
         * @param dir direction, defaults to `left`
         * @param columnIndex If specified, determines which column the text should begin or end on, whether left- or right-aligned, respectively.
         */
        buildColumnInformation(templateRowColumn, rowIndex, dir = 'left', columnIndex) {
            const content = this.parseContent(templateRowColumn);
            let charCount = 0;
            // count all letters
            content.forEach(x => {
                var _a, _b;
                charCount += (_b = (_a = x.content) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
            });
            charCount = Math.min(charCount, this.options.screenCellWidth);
            // set start pos
            let charGridColumn = 0;
            if (columnIndex !== undefined) {
                charGridColumn = dir === 'right' ? columnIndex - charCount + 1 : columnIndex;
            }
            else if (dir === 'right') {
                charGridColumn = this.options.screenCellWidth - charCount;
            }
            else if (dir == 'center') {
                charGridColumn = Math.round((((this.options.screenCellWidth - 1) / 2) - (charCount / 2)));
            }
            // build data struct
            const row = this.columnData[rowIndex];
            content.forEach(styleGroup => {
                const letters = styleGroup.content.split('');
                letters.forEach((char) => {
                    const colInfo = row[charGridColumn];
                    if (charGridColumn < this.options.screenCellWidth && colInfo !== undefined) {
                        colInfo.styles = styleGroup.styles;
                        colInfo.content = char === '' ? ' ' : char;
                    }
                    charGridColumn++;
                });
            });
        }
        /**
         * Parses content into intermediate information blocks
         * @param content the content to parse
         * @returns a list of information blocks
         */
        parseContent(content) {
            const resultInfo = [];
            // if it starts with a bracket its probably empty
            if (content.startsWith('[')) {
                return resultInfo;
            }
            // eslint-disable-next-line no-useless-escape
            const regex = /([^\[\]\n]+)(\[[^\[\]\n]+\])*/g;
            let match = regex.exec(content);
            if (match) {
                while (match != null) {
                    const el = {
                        content: match[1].replace('__LSB', '[').replace('__RSB', ']'),
                        styles: '',
                    };
                    if (match[2]) {
                        // eslint-disable-next-line no-useless-escape
                        const classes = match[2].match(/[^\s\[\]]+/g);
                        if (classes) {
                            el.styles = classes.join(' ');
                        }
                    }
                    resultInfo.push(el);
                    match = regex.exec(content);
                }
            }
            return resultInfo;
        }
        /**
         * Builds the template for the whole container.
         * @param defaultAlternatingLayout if the rows should alternate
         * @returns the container template
         */
        createContainerTemplate(defaultAlternatingLayout = true) {
            // create container
            const container = document.createElement('div');
            container.id = 'fmc-container';
            this.colTemplate = this.createColumnTemplate();
            this.rowTemplate = this.createRowTemplate();
            // const preRenderRows = Date.now();
            for (let r = 0; r < this.options.screenCellHeight; r++) {
                // rows
                const row = this.rowTemplate.cloneNode(true);
                if (defaultAlternatingLayout && r % 2 == 1) {
                    row.classList.remove('d-text');
                    row.classList.add('s-text');
                }
                container.appendChild(row);
            }
            return container;
        }
        /**
         * Builds the template for a single row.
         * @returns the row template
         */
        createRowTemplate() {
            // create row template
            const rowTemplate = document.createElement('div');
            rowTemplate.classList.add('fmc-row');
            rowTemplate.classList.add('d-text');
            const cellHeight = this.options.screenPXHeight / this.options.screenCellHeight;
            rowTemplate.style.height = `${cellHeight}px`;
            rowTemplate.style.lineHeight = `${cellHeight}px`;
            const cellWidth = this.options.screenPXWidth / this.options.screenCellWidth;
            const lostDecimalWidth = cellWidth - Math.trunc(cellWidth);
            const paddingElemLeft = document.createElement('span');
            paddingElemLeft.style.width = `${Math.ceil((lostDecimalWidth * this.options.screenCellWidth) / 2)}px`;
            paddingElemLeft.style.display = 'inline-block';
            rowTemplate.appendChild(paddingElemLeft);
            // create spans in row template
            for (let c = 0; c < this.options.screenCellWidth; c++) {
                const col = this.colTemplate.cloneNode(true);
                rowTemplate.appendChild(col);
            }
            return rowTemplate;
        }
        /**
         * Builds the template for a single column.
         * @returns the column template
         */
        createColumnTemplate() {
            // create column template
            const colEl = document.createElement('div');
            colEl.classList.add('fmc-letter');
            colEl.style.width = `${this.options.screenPXWidth / this.options.screenCellWidth}px`;
            colEl.style.height = `${this.options.screenPXHeight / this.options.screenCellHeight}px`;
            colEl.style.lineHeight = `${this.options.screenPXHeight / this.options.screenCellHeight}px`;
            colEl.textContent = ' ';
            return colEl;
        }
        /**
         * Builds the data structure representing the content and looks.
         * @returns the data structure
         */
        createColumnInformation() {
            const columnInformation = [];
            for (let r = 0; r < this.options.screenCellHeight; r++) {
                const rowColumns = [];
                for (let c = 0; c < this.options.screenCellWidth; c++) {
                    const colInfo = {
                        content: ' ',
                        styles: '',
                    };
                    rowColumns.push(colInfo);
                }
                columnInformation.push(rowColumns);
            }
            return columnInformation;
        }
    }

    /**
     * A utility class for working with colors.
     */
    class ColorUtils {
        /**
         * Reverses the order of concatenation of the red, green, and blue components in a numeric hex color. A value equal
         * to `(red << 16) + (green << 8) + (blue << 0)` will be converted to `(blue << 16) + (green << 8) + (red << 0)` and
         * vice versa.
         * @param hex The numeric hex color to reverse.
         * @returns The specified numeric hex color with the concatenation order of red, green, and blue components reversed.
         */
        static reverseHexNumber(hex) {
            return ((hex & ColorUtils.BYTE_2_MASK) >> 16) | (hex & ColorUtils.BYTE_1_MASK) | ((hex & ColorUtils.BYTE_0_MASK) << 16);
        }
        /**
         * Converts a hex color string to its numeric representation. The numeric hex color is equal to
         * `(red << 16) + (green << 8) + (blue << 0)`.
         * @param hex The hex color string to convert. The string should be a sequence of exactly six hexadecimal characters
         * optionally prefixed with `#`.
         * @param reverse Whether to reverse the order in which the red, green, and blue components are concatenated in the
         * numeric representation. If `true`, then the numeric hex color will equal
         * `(blue << 16) + (green << 8) + (red << 0)`. Defaults to `false`.
         * @returns The numeric representation of the specified hex color string.
         * @throws Error if the string is improperly formatted.
         */
        static hexStringToNumber(hex, reverse = false) {
            if (!ColorUtils.HEX_REGEX.test(hex)) {
                throw new Error(`ColorUtils: invalid hex string: '${hex}'`);
            }
            if (hex.indexOf('#') === 0) {
                hex = hex.substring(1);
            }
            const value = parseInt(hex, 16);
            if (reverse) {
                return ColorUtils.reverseHexNumber(value);
            }
            else {
                return value;
            }
        }
        /**
         * Converts a numeric representation of a hex color to a string.
         * @param hex The numeric hex color to convert. The color will be interpreted as an integer with the value
         * `(red << 16) + (green << 8) + (blue << 0)`. Bits with value greater than or equal to `1 << 24` will be discarded.
         * @param reverse Whether to interpret the numeric hex color with reversed components. If `true`, then the numeric
         * hex color will be interpreted as `(blue << 16) + (green << 8) + (red << 0)`. Defaults to `false`.
         * @returns The string form of the specified numeric hex color.
         */
        static hexNumberToString(hex, reverse = false) {
            if (reverse) {
                hex = ColorUtils.reverseHexNumber(hex);
            }
            return (hex & ColorUtils.MASK_24).toString(16).padStart(6, '0');
        }
        /**
         * Converts a hex color to red, green, and blue (RGB) components. Each component is expressed in the range 0 to 255.
         * @param hex The hex color to convert, either as a string containing a sequence of exactly six hexadecimal
         * characters optionally prefixed with `#` or a numeric value equal to `(red << 16) + (green << 8) + (blue << 0)`
         * (bits with value greater than or equal to `1 << 24` will be discarded).
         * @param out The array to which to write the results.
         * @param reverse Whether to interpret the numeric hex color with reversed components. If `true`, then the numeric
         * hex color will be interpreted as `(blue << 16) + (green << 8) + (red << 0)`. Ignored if `hex` is a string.
         * Defaults to `false`.
         * @returns The red, green, and blue (RGB) components of the specified hex color, as `[r, g, b]`.
         * @throws Error if the specified hex color is an improperly formatted string.
         */
        static hexToRgb(hex, out, reverse = false) {
            let hexValue;
            if (typeof hex === 'string') {
                hexValue = ColorUtils.hexStringToNumber(hex);
            }
            else {
                hexValue = Math.abs(Math.trunc(hex));
                if (reverse) {
                    hexValue = ColorUtils.reverseHexNumber(hex);
                }
            }
            out[0] = (hexValue >> 16) % 256;
            out[1] = (hexValue >> 8) % 256;
            out[2] = hexValue % 256;
            return out;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static rgbToHex(arg1, arg2, arg3, arg4) {
            let r, g, b;
            let reverse;
            if (typeof arg1 === 'number') {
                r = arg1;
                g = arg2;
                b = arg3;
                reverse = arg4;
            }
            else {
                r = arg1[0];
                g = arg1[1];
                b = arg1[2];
                reverse = arg2;
            }
            r = MathUtils.clamp(Math.round(r), 0, 255);
            g = MathUtils.clamp(Math.round(g), 0, 255);
            b = MathUtils.clamp(Math.round(b), 0, 255);
            return reverse
                ? (b << 16) | (g << 8) | r
                : (r << 16) | (g << 8) | b;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static rgbToHsl(arg1, arg2, arg3, arg4) {
            let r, g, b;
            let out;
            if (typeof arg1 === 'number') {
                r = arg1;
                g = arg2;
                b = arg3;
                out = arg4;
            }
            else {
                r = arg1[0];
                g = arg1[1];
                b = arg1[2];
                out = arg2;
            }
            r = MathUtils.clamp(r / 255, 0, 1);
            g = MathUtils.clamp(g / 255, 0, 1);
            b = MathUtils.clamp(b / 255, 0, 1);
            const max = Math.max(r, g, b);
            const min = Math.min(r, g, b);
            const chroma = max - min;
            const l = (max + min) / 2;
            const s = l === 0 || l === 1 ? 0 : chroma / (1 - Math.abs(max + min - 1));
            const h = 60 * (chroma === 0 ? 0
                : max === r ? ((g - b) / chroma) % 6
                    : max === g ? (b - r) / chroma + 2
                        : (r - g) / chroma + 4);
            out[0] = h;
            out[1] = s;
            out[2] = l;
            return out;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static hslToRgb(arg1, arg2, arg3, arg4) {
            let h, s, l;
            let out;
            if (typeof arg1 === 'number') {
                h = arg1;
                s = arg2;
                l = arg3;
                out = arg4;
            }
            else {
                h = arg1[0];
                s = arg1[1];
                l = arg1[2];
                out = arg2;
            }
            h = h % 360;
            s = MathUtils.clamp(s, 0, 1);
            l = MathUtils.clamp(l, 0, 1);
            const chroma = s * (1 - Math.abs(2 * l - 1));
            const side = h / 60;
            const x = chroma * (1 - Math.abs(side % 2 - 1));
            const m = l - chroma / 2;
            if (side <= 1) {
                out[0] = chroma;
                out[1] = x;
                out[2] = 0;
            }
            else if (side <= 2) {
                out[0] = x;
                out[1] = chroma;
                out[2] = 0;
            }
            else if (side <= 3) {
                out[0] = 0;
                out[1] = chroma;
                out[2] = x;
            }
            else if (side <= 4) {
                out[0] = 0;
                out[1] = x;
                out[2] = chroma;
            }
            else if (side <= 5) {
                out[0] = x;
                out[1] = 0;
                out[2] = chroma;
            }
            else {
                out[0] = chroma;
                out[1] = 0;
                out[2] = x;
            }
            out[0] = Math.round((out[0] + m) * 255);
            out[1] = Math.round((out[1] + m) * 255);
            out[2] = Math.round((out[2] + m) * 255);
            return out;
        }
        /**
         * Converts a hex color to hue, saturation, and lightness (HSL) components. Hue is expressed in degrees (0 to 360),
         * and saturation and lightness are expressed as fractions (0 to 1).
         * @param hex The hex color to convert, either as a string containing a sequence of exactly six hexadecimal
         * characters optionally prefixed with `#` or a numeric value equal to `(red << 16) + (green << 8) + (blue << 0)`
         * (bits with value greater than or equal to `1 << 24` will be discarded).
         * @param out The array to which to write the results.
         * @param reverse Whether to interpret the numeric hex color with reversed components. If `true`, then the numeric
         * hex color will be interpreted as `(blue << 16) + (green << 8) + (red << 0)`. Ignored if `hex` is a string.
         * Defaults to `false`.
         * @returns The hue, saturation, and lightness (HSL) color components of the specified hex color, as `[h, s, l]`.
         * @throws Error if the specified hex color is an improperly formatted string.
         */
        static hexToHsl(hex, out, reverse = false) {
            const rgb = ColorUtils.hexToRgb(hex, ColorUtils.rgbCache, reverse);
            return ColorUtils.rgbToHsl(rgb, out);
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static hslToHex(arg1, arg2, arg3, arg5) {
            if (typeof arg1 === 'number') {
                return ColorUtils.rgbToHex(ColorUtils.hslToRgb(arg1, arg2, arg3, ColorUtils.rgbCache), arg5);
            }
            else {
                return ColorUtils.rgbToHex(ColorUtils.hslToRgb(arg1, ColorUtils.rgbCache), arg2);
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static interpolateHex(color1, color2, fraction, out) {
            const useString = typeof color1 === 'string';
            const color1Number = useString ? ColorUtils.hexStringToNumber(color1) : color1;
            const color2Number = useString ? ColorUtils.hexStringToNumber(color2) : color2;
            const interp = typeof fraction === 'object'
                ? ColorUtils.interpolateRgbSpace(color1Number, color2Number, fraction, out)
                : ColorUtils.interpolateRgbSpace(color1Number, color2Number, fraction);
            if (useString) {
                if (Array.isArray(interp)) {
                    for (let i = 0; i < interp.length; i++) {
                        interp[i] = ColorUtils.hexNumberToString(interp[i]);
                    }
                    return interp;
                }
                else {
                    return ColorUtils.hexNumberToString(interp);
                }
            }
            else {
                return interp;
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static interpolateRgbSpace(color1, color2, fraction, out) {
            const r1 = (color1 & ColorUtils.BYTE_2_MASK) >> 16;
            const g1 = (color1 & ColorUtils.BYTE_1_MASK) >> 8;
            const b1 = (color1 & ColorUtils.BYTE_0_MASK);
            const r2 = (color2 & ColorUtils.BYTE_2_MASK) >> 16;
            const g2 = (color2 & ColorUtils.BYTE_1_MASK) >> 8;
            const b2 = (color2 & ColorUtils.BYTE_0_MASK);
            if (typeof fraction === 'number') {
                const r = Math.round(MathUtils.lerp(fraction, 0, 1, r1, r2, true, true));
                const g = Math.round(MathUtils.lerp(fraction, 0, 1, g1, g2, true, true));
                const b = Math.round(MathUtils.lerp(fraction, 0, 1, b1, b2, true, true));
                return (r << 16) | (g << 8) | b;
            }
            else {
                out !== null && out !== void 0 ? out : (out = []);
                for (let i = 0; i < fraction.length; i++) {
                    const r = Math.round(MathUtils.lerp(fraction[i], 0, 1, r1, r2, true, true));
                    const g = Math.round(MathUtils.lerp(fraction[i], 0, 1, g1, g2, true, true));
                    const b = Math.round(MathUtils.lerp(fraction[i], 0, 1, b1, b2, true, true));
                    out[i] = (r << 16) | (g << 8) | b;
                }
                out.length = fraction.length;
                return out;
            }
        }
        /**
         * Creates a function that linearly interpolates between two colors in the RGB colorspace.
         * @param color1 The first color to interpolate between.
         * @param color2 The second color to interpolate between.
         * @returns A function that linearly interpolates between the two specified colors in the RGB colorspace.
         */
        static gradientHex(color1, color2) {
            const useString = typeof color1 === 'string';
            const color1Number = useString ? ColorUtils.hexStringToNumber(color1) : color1;
            const color2Number = useString ? ColorUtils.hexStringToNumber(color2) : color2;
            if (useString) {
                const interpFunc = ColorUtils.gradientRgbSpace(color1Number, color2Number);
                return ((fraction) => {
                    return ColorUtils.hexNumberToString(interpFunc(fraction));
                });
            }
            else {
                return ColorUtils.gradientRgbSpace(color1Number, color2Number);
            }
        }
        /**
         * Creates a function that linearly interpolates between two numeric hex colors in the RGB colorspace.
         * @param color1 The first color to interpolate between.
         * @param color2 The second color to interpolate between.
         * @returns A function that linearly interpolates between the two specified colors in the RGB colorspace.
         */
        static gradientRgbSpace(color1, color2) {
            const r1 = (color1 & ColorUtils.BYTE_2_MASK) >> 16;
            const g1 = (color1 & ColorUtils.BYTE_1_MASK) >> 8;
            const b1 = (color1 & ColorUtils.BYTE_0_MASK);
            const r2 = (color2 & ColorUtils.BYTE_2_MASK) >> 16;
            const g2 = (color2 & ColorUtils.BYTE_1_MASK) >> 8;
            const b2 = (color2 & ColorUtils.BYTE_0_MASK);
            return (fraction) => {
                const r = Math.round(MathUtils.lerp(fraction, 0, 1, r1, r2, true, true));
                const g = Math.round(MathUtils.lerp(fraction, 0, 1, g1, g2, true, true));
                const b = Math.round(MathUtils.lerp(fraction, 0, 1, b1, b2, true, true));
                return (r << 16) | (g << 8) | b;
            };
        }
    }
    ColorUtils.HEX_REGEX = /^#?([0-9a-fA-F]{6})$/;
    ColorUtils.BYTE_0_MASK = ((1 << 8) - 1) << 0;
    ColorUtils.BYTE_1_MASK = ((1 << 8) - 1) << 8;
    ColorUtils.BYTE_2_MASK = ((1 << 8) - 1) << 16;
    ColorUtils.MASK_24 = (1 << 24) - 1;
    ColorUtils.rgbCache = new Float64Array(3);

    /**
     * A class for caching images.
     * Do your own instrument specific implementation with an init() method
     * that will add images to cache on instrument load to prefill the cache.
     * @class ImageCache
     */
    class ImageCache {
        /**
         * Loads the image from the url and adds it to the cache.
         * @static
         * @param key The image key to access it later.
         * @param url The url to load the image from.
         */
        static addToCache(key, url) {
            if (this.cache[key] === undefined) {
                const img = new Image();
                img.src = url;
                this.cache[key] = img;
            }
        }
        /**
         * Gets a cached image element.
         * @static
         * @param key The key of the cached image.
         * @returns The cached image element.
         */
        static get(key) {
            return this.cache[key];
        }
    }
    ImageCache.cache = {};

    /** A collection of handy SVG functions. */
    class SVGUtils {
        /**
         * Creates an arc using an SVG path.
         * From https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
         * @param x Arc center x position.
         * @param y Arc center y position.
         * @param radius Arc radius.
         * @param startAngle Arc start angle.
         * @param endAngle Arc end angle.
         * @returns The d value for and SVG path element.
         */
        static describeArc(x, y, radius, startAngle, endAngle) {
            const start = polarToCartesian(x, y, radius, endAngle);
            const end = polarToCartesian(x, y, radius, startAngle);
            const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
            const d = [
                'M', start[0], start[1],
                'A', radius, radius, 0, largeArcFlag, 0, end[0], end[1]
            ].join(' ');
            return d;
        }
    }
    // eslint-disable-next-line jsdoc/require-jsdoc
    function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
        const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
        return new Float64Array([
            centerX + (radius * Math.cos(angleInRadians)),
            centerY + (radius * Math.sin(angleInRadians))
        ]);
    }

    /* eslint-disable max-len */
    /**
     * A utility class for creating time formatters.
     *
     * Each time formatter is a function which generates output strings from input time values, expressed as UNIX
     * timestamps in milliseconds. The formatting behavior of a formatter is defined by its format template and options.
     *
     * Please refer to the {@link DateTimeFormatterOptions} type documentation for more information on individual
     * formatting options.
     *
     * Format templates are strings which contain zero or more fragments enclosed by curly braces (`{}`); For a given
     * format template, an output string is generated from an input duration by replacing each fragment in the template
     * with a string generated from the input. The parts of the template string that are not contained in any fragment are
     * passed to the output unchanged. Each fragment defines how its replacement string is generated. There are two types
     * of fragments:
     * * Integer fragment. In EBNF notation, these take the form `{x}` where `x = 'M' | 'd' | 'w'`. Each numeric fragment
     * is replaced with an integer representation of the month (`M`), day of month (`d`), or day of week (`w`) part of the
     * input time. The number of `x` characters in the definition controls the number of leading zeroes with which the
     * output will be padded.
     * * Numeric fragment. In EBNF notation, these take the form `{x}, ['?'], ['.', [{x}], ['(', {x}, ')']]` where
     * `x = 'H' | 'h' | 'm' | 's'`. Each numeric fragment is replaced with a numeric representation of the hour-24 (`H`),
     * hour-12 (`h`), minute (`m`), or second (`s`) part of the input time. The number of `x` characters in the definition
     * controls the number of leading zeroes with which the output will be padded. If the optional `'?'` character is
     * present, the output will drop all digits to the left of the decimal point if all such digits are equal to 0. The
     * total number of `x` characters to the right of the decimal point in the definition controls the decimal precision of
     * the output. Trailing zeroes to the right of the decimal point will be added to the output to a number of decimal
     * places equal to the number of non-parenthetical `x` characters to the right of the decimal point in the definition.
     * If there are no `x` characters to the right of the decimal point in the definition, then the output will have
     * infinite decimal precision with no extraneous trailing zeroes. Rounding behavior is always round down.
     * * Year fragment. In EBNF notation, these take the form `'YY' | 'YYYY'`. Each year fragment is replaced with either
     * the two-digit (`YY`) or unlimited-digit (`YYYY`) year of the input time.
     * * Month fragment. In EBNF notation, these take the form `('mon', ['.']) | ('MON', ['.']) | 'month' | 'MONTH'`. Each
     * month fragment is replaced with the name of the month of the input time. The case of the definition determines the
     * case of the output. `mon` will use abbreviated names. The presence of the optional `'.'` character will add a period
     * to the end of the abbreviated names.
     * * Day-of-week fragment. In EBNF notation, these take the form `('dy', ['.']) | ('DY', ['.']) | 'day' | 'DAY'`. Each
     * day-of-week fragment is replaced with the name of the day-of-week of the input time. The case of the definition
     * determines the case of the output. `dy` will use abbreviated names. The presence of the optional `'.'` character
     * will add a period to the end of the abbreviated names.
     * * AM/PM fragment. In EBNF notation, these take the form `'am' | 'a.m.' | 'AM' | 'A.M.'`. Each AM/PM fragment is
     * replaced with an AM/PM string depending on the time of day of the input. The case of the definition determines the
     * case of the output. Use of periods in the definition will add periods to the output.
     *
     * @example <caption>Formatting to a date</caption>
     * const formatter = DateTimeFormatter.create('{dd}-{MM}-{YY}');
     * formatter(0);              // 01-01-70
     * formatter(1597723200000);  // 18-08-20
     *
     * @example <caption>Formatting to a time (24-hr)</caption>
     * const formatter = DateTimeFormatter.create('{HH}:{mm}:{ss}');
     * formatter(0);              // 00:00:00
     * formatter(5145000);        // 01:25:45
     * formatter(57600000);       // 16:00:00
     *
     * @example <caption>Formatting to a time (12-hr)</caption>
     * const formatter = DateTimeFormatter.create('{hh}:{mm}:{ss} {am}');
     * formatter(0);              // 12:00:00 am
     * formatter(5145000);        // 01:25:45 am
     * formatter(57600000);       // 04:00:00 pm
     *
     * @example <caption>Formatting to a time with decimals</caption>
     * const formatter = DateTimeFormatter.create('{H}:{mm.m(m)}');
     * formatter(0);              // 0:00.0
     * formatter(5145000);        // 1:25.75
     *
     * @example <caption>Formatting to ISO 8601</caption>
     * const formatter = DateTimeFormatter.create('{YYYY}-{MM}-{dd}T{HH}:{mm}:{ss}');
     * formatter(0);              // 1970-01-01T00:00:00
     * formatter(1597723200000);  // 2020-08-18T04:00:00
     */
    class DateTimeFormatter {
        /**
         * Creates a function which formats times, expressed as UNIX timestamps in milliseconds, to strings. The formatting
         * behavior of the function is defined by a specified format template and options. For more information on format
         * templates and their syntax, please refer to the {@link DateTimeFormatter} class documentation. For more
         * information on individual formatting options, please refer to the {@link DateTimeFormatterOptions} type
         * documentation.
         * @param format A template defining how the function formats durations.
         * @param options Options to customize the formatter. Options not explicitly defined will be set to the following
         * default values:
         * * `monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']`
         * * `monthNamesShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']`
         * * `dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']`
         * * `dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']`
         * * `nanString = 'NaN'`
         * @returns A function which formats times, expressed as UNIX timestamps in milliseconds, to strings.
         */
        static create(format, options) {
            const optsToUse = this.resolveOptions(options);
            const builder = DateTimeFormatter.createBuilder(format, optsToUse);
            const date = new Date();
            const built = Array.from(builder, () => '');
            if (optsToUse.cache) {
                let cachedInput = undefined;
                let cachedOutput = undefined;
                return (time) => {
                    if (isNaN(time)) {
                        return optsToUse.nanString;
                    }
                    const roundedInput = Math.floor(time);
                    if (cachedInput !== undefined && cachedOutput !== undefined && roundedInput === cachedInput) {
                        return cachedOutput;
                    }
                    cachedInput = roundedInput;
                    date.setTime(roundedInput);
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](date);
                    }
                    return cachedOutput = built.join('');
                };
            }
            else {
                return (time) => {
                    if (isNaN(time)) {
                        return optsToUse.nanString;
                    }
                    date.setTime(Math.floor(time));
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](date);
                    }
                    return built.join('');
                };
            }
        }
        /**
         * Resolves a full set of options from a partial options object. Any option not explicitly defined by the partial
         * options object will revert to its default value.
         * @param options A partial options object.
         * @returns A new options object containing the full set of options resolved from the specified partial options
         * object.
         */
        static resolveOptions(options) {
            var _a;
            const resolved = Object.assign({}, options);
            for (const key in DateTimeFormatter.DEFAULT_OPTIONS) {
                (_a = resolved[key]) !== null && _a !== void 0 ? _a : (resolved[key] = DateTimeFormatter.DEFAULT_OPTIONS[key]);
            }
            return resolved;
        }
        /**
         * Creates an output string builder from a format template and options.
         * @param format A format template.
         * @param options Formatting options.
         * @returns An output string builder which conforms to the specified format template and options.
         */
        static createBuilder(format, options) {
            const split = format.split(DateTimeFormatter.FORMAT_REGEXP);
            return split.map((string) => {
                if (string.match(DateTimeFormatter.FORMAT_REGEXP)) {
                    return DateTimeFormatter.parseFragment(string.substring(1, string.length - 1), options);
                }
                else {
                    return () => string;
                }
            });
        }
        /**
         * Parses a format template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment. If the fragment is malformed, this method returns a function which always
         * generates an empty string.
         * @param fragment A format template fragment definition.
         * @param options Formatting options.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the template fragment.
         */
        static parseFragment(fragment, options) {
            const match = fragment.match(DateTimeFormatter.FRAGMENT_REGEXP);
            if (match) {
                if (match[1]) {
                    return DateTimeFormatter.parseIntFragment(match);
                }
                else if (match[3]) {
                    return DateTimeFormatter.parseNumFragment(match);
                }
                else if (match[10]) {
                    return DateTimeFormatter.parseYearFragment(match);
                }
                else if (match[11]) {
                    return DateTimeFormatter.parseMonthFragment(match, options);
                }
                else if (match[12]) {
                    return DateTimeFormatter.parseDayFragment(match, options);
                }
                else if (match[13]) {
                    return DateTimeFormatter.parseAMPMFragment(match);
                }
            }
            console.warn(`DateTimeFormatter: discarding fragment due to invalid syntax: {${fragment}}`);
            return () => '';
        }
        /**
         * Parses an integer template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment.
         * @param match An integer template fragment, as a regular expression match.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the integer template fragment.
         */
        static parseIntFragment(match) {
            const [
            /* 0 */ , fragmentMatch, // Matches the full fragment
            unitMatch // Matches the unit char ('M', 'd', 'w')
            ] = match;
            const intGetter = DateTimeFormatter.INT_GETTERS[unitMatch];
            const pad = fragmentMatch.length;
            return (date) => {
                return intGetter(date).toString().padStart(pad, '0');
            };
        }
        /**
         * Parses a numeric template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment.
         * @param match A numeric template fragment, as a regular expression match.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the numeric template fragment.
         */
        static parseNumFragment(match) {
            var _a, _b;
            const [
            /* 0 */ , /* 1 */ , /* 2 */ , /* 3 */ , leftMatch, // Matches unit chars to the left of the decimal point
            unitMatch, // Matches the unit char ('h', 'm', 's', etc)
            leftOptionalMatch, // Matches the question mark just to the left of the decimal point
            rightMatch, // Matches the decimal point and all chars to the right
            rightForcedMatch, // Matches unit chars to the right of the decimal point not surrounded by parens
            rightOptionalMatch // Matches unit chars to the right of the decimal point surrounded by parens
            ] = match;
            const numGetter = DateTimeFormatter.NUM_GETTERS[unitMatch];
            const numFractionGetter = DateTimeFormatter.NUM_FRACTION_GETTERS[unitMatch];
            const pad = leftMatch.length;
            const dropZero = !!leftOptionalMatch;
            const formatLeftFunc = dropZero
                ? ((date) => {
                    const num = numGetter(date);
                    return num === 0 ? '' : num.toString().padStart(pad, '0');
                }) : ((date) => numGetter(date).toString().padStart(pad, '0'));
            if (rightMatch) {
                if (rightMatch.length === 1) {
                    // Unlimited decimal precision
                    return (date) => {
                        return `${formatLeftFunc(date)}${numFractionGetter(date).toString().substring(1)}`;
                    };
                }
                const forcedDecimalPlaces = (_a = rightForcedMatch === null || rightForcedMatch === void 0 ? void 0 : rightForcedMatch.length) !== null && _a !== void 0 ? _a : 0;
                const unforcedDecimalPlaces = (_b = rightOptionalMatch === null || rightOptionalMatch === void 0 ? void 0 : rightOptionalMatch.length) !== null && _b !== void 0 ? _b : 0;
                const totalDecimalPlaces = forcedDecimalPlaces + unforcedDecimalPlaces;
                const factor = Math.pow(10, totalDecimalPlaces);
                return (date) => {
                    const decimal = numFractionGetter(date);
                    const decimalRounded = Math.floor(decimal * factor) / factor;
                    return `${formatLeftFunc(date)}.${decimalRounded.toString().substring(2).padEnd(forcedDecimalPlaces, '0')}`;
                };
            }
            else {
                return formatLeftFunc;
            }
        }
        /**
         * Parses a year template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment.
         * @param match A year template fragment, as a regular expression match.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the year template fragment.
         */
        static parseYearFragment(match) {
            if (match[10].length === 2) {
                // YY
                return (date) => (date.getUTCFullYear() % 100).toString().padStart(2, '0');
            }
            else {
                // YYYY
                return (date) => date.getUTCFullYear().toString();
            }
        }
        /**
         * Parses a month template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment.
         * @param match A month template fragment, as a regular expression match.
         * @param options Formatting options.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the month template fragment.
         */
        static parseMonthFragment(match, options) {
            const fragmentMatch = match[11];
            const isUpperCase = fragmentMatch[0] === 'M';
            if (fragmentMatch.length === 3) {
                // mon
                const text = isUpperCase ? options.monthNamesShort.map(str => str.toUpperCase()) : options.monthNamesShort;
                return (date) => text[date.getUTCMonth()];
            }
            else if (fragmentMatch.length === 4) {
                // mon.
                const text = isUpperCase ? options.monthNamesShort.map(str => str.toUpperCase()) : options.monthNamesShort;
                return (date) => {
                    const month = date.getUTCMonth();
                    return `${text[month]}${options.monthNamesShort[month] === options.monthNames[month] ? '' : '.'}`;
                };
            }
            else {
                // month
                const text = isUpperCase ? options.monthNames.map(str => str.toUpperCase()) : options.monthNames;
                return (date) => text[date.getUTCMonth()];
            }
        }
        /**
         * Parses a day-of-week template fragment and returns a function which generates a string from an input time
         * according to the rules defined by the fragment.
         * @param match A day-of-week template fragment, as a regular expression match.
         * @param options Formatting options.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the day-of-week template fragment.
         */
        static parseDayFragment(match, options) {
            const fragmentMatch = match[12];
            const isUpperCase = fragmentMatch[0] === 'D';
            if (fragmentMatch === 'dy') {
                const text = isUpperCase ? options.dayNamesShort.map(str => str.toUpperCase()) : options.dayNamesShort;
                return (date) => text[date.getUTCDay()];
            }
            else if (fragmentMatch === 'dy.') {
                const text = isUpperCase ? options.dayNamesShort.map(str => str.toUpperCase()) : options.dayNamesShort;
                return (date) => {
                    const day = date.getUTCDay();
                    return `${text[day]}${options.dayNamesShort[day] === options.dayNames[day] ? '' : '.'}`;
                };
            }
            else {
                // day
                const text = isUpperCase ? options.dayNames.map(str => str.toUpperCase()) : options.dayNames;
                return (date) => text[date.getUTCDay()];
            }
        }
        /**
         * Parses an am/pm template fragment and returns a function which generates a string from an input time according
         * to the rules defined by the fragment.
         * @param match An am/pm template fragment, as a regular expression match.
         * @returns A function which generates a string from an input time, expressed as a UNIX timestamp in milliseconds,
         * according to the rules defined by the am/pm template fragment.
         */
        static parseAMPMFragment(match) {
            const fragmentMatch = match[13];
            const isUpperCase = fragmentMatch[0] === 'A';
            const usePeriod = fragmentMatch.length > 2;
            let text = usePeriod ? ['a.m.', 'p.m.'] : ['am', 'pm'];
            if (isUpperCase) {
                text = text.map(str => str.toUpperCase());
            }
            return (date) => text[Math.floor(date.getUTCHours() / 12)];
        }
    }
    DateTimeFormatter.FORMAT_REGEXP = /({[^{}]*})/;
    DateTimeFormatter.FRAGMENT_REGEXP = /^(?:(([Mdw])\2*)|((([Hhms])\5*)(\?)?(?:(\.(\5*)(?:\((\5+)\))?)?))|(YY|YYYY)|(mon\.?|month|MON\.?|MONTH)|(dy\.?|day|DY\.?|DAY)|(am|AM|a\.m\.|A\.M\.))$/;
    DateTimeFormatter.INT_GETTERS = {
        ['w']: (date) => date.getUTCDay() + 1,
        ['d']: (date) => date.getUTCDate(),
        ['M']: (date) => date.getUTCMonth() + 1
    };
    DateTimeFormatter.NUM_GETTERS = {
        ['s']: (date) => date.getUTCSeconds(),
        ['m']: (date) => date.getUTCMinutes(),
        ['h']: (date) => 12 - (24 - date.getUTCHours()) % 12,
        ['H']: (date) => date.getUTCHours()
    };
    DateTimeFormatter.NUM_FRACTION_GETTERS = {
        ['s']: (date) => (date.getTime() % 1000) / 1000,
        ['m']: (date) => (date.getTime() % 60000) / 60000,
        ['h']: (date) => (date.getTime() % 3600000) / 3600000,
        ['H']: (date) => (date.getTime() % 3600000) / 3600000
    };
    DateTimeFormatter.DEFAULT_OPTIONS = {
        monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
        monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
        dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
        dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        nanString: 'NaN',
        cache: false
    };

    /** A utility class for creating AIRAC cycle formatters. */
    class AiracCycleFormatter {
        /** Disable the ctor. */
        constructor() { }
        /**
         * Creates a new AIRAC cycle formatter.
         * @param format The format to use, with the following possible items to be replaced:
         * - `{CC}`: is replaced with the 2-digit cycle number e.g. 01,
         * - `{YYCC}`: is replaced with the 4-digit cycle number, including the effective year and the cycle number e.g. 2401,
         * - `{eff(dateTimeFormat)}`: is replaced by the effective date,
         * formatted by {@link DateTimeFormatter} with the given `dateTimeFormat`,
         * - `{exp(dateTimeFormat)}`: is replaced by the expiration date,
         * formatted by {@link DateTimeFormatter} with the given `dateTimeFormat`,
         * - `{expMinus(dateTimeFormat)}`: is replaced by the expiration date minus one second (i.e. 1 second before midnight the previous day),
         * formatted by {@link DateTimeFormatter} with the given `dateTimeFormat`.
         *
         * Note: Some avionics use `exp` (overlapping dates) while others use `expMinus` (non-overlapping dates) for the expiration dates.\
         * Make sure to check what yours should do.
         * @example The format `{YYCC} {eff({dd}-{MON})}/{exp({dd}-{MON})}` gives "2309 07-SEP/05-OCT" for AIRAC cycle 2309.
         * @example The format `{YYCC} {eff({dd}-{MON})}/{expMinus({dd}-{MON})}` gives "2309 07-SEP/04-OCT" for AIRAC cycle 2309.
         * @throws If the format is invalid, an exception will be thrown.
         * @returns The formatter function.
         */
        static create(format) {
            const split = format.split(AiracCycleFormatter.FORMAT_REGEXP);
            const formatFragments = split.map((string) => {
                if (string.match(AiracCycleFormatter.FORMAT_REGEXP)) {
                    return AiracCycleFormatter.parseFragment(string.substring(1, string.length - 1));
                }
                else {
                    return string;
                }
            });
            return (cycle) => formatFragments.map((f) => typeof f === 'function' ? f(cycle) : f).join('');
        }
        /**
         * Parses a fragment into a function that resolves it to a string.
         * @param string The format fragment.
         * @returns The resolver function.
         * @throws When the fragment is invalid.
         */
        static parseFragment(string) {
            var _a;
            const match = string.match(this.FRAGMENT_DATE_REGEXP);
            if (match !== null) {
                // this is a date fragment
                const dateType = match[1];
                const dateFormat = match[2];
                const dateFormatter = (_a = AiracCycleFormatter.dateFormatterCache.get(dateFormat)) !== null && _a !== void 0 ? _a : AiracCycleFormatter.dateFormatterCache.set(dateFormat, DateTimeFormatter.create(dateFormat)).get(dateFormat);
                switch (dateType) {
                    case 'eff':
                        return (cycle) => dateFormatter(cycle.effectiveTimestamp);
                    case 'exp':
                        return (cycle) => dateFormatter(cycle.expirationTimestamp);
                    case 'expMinus':
                        return (cycle) => dateFormatter(cycle.expirationTimestamp - 1000);
                }
            }
            else {
                // this is a cycle number fragment
                switch (string) {
                    case 'CC':
                        return (cycle) => cycle.cycleString;
                    case 'YYCC':
                        return (cycle) => cycle.ident;
                }
            }
            throw new Error(`AiracCycleFormatter: Invalid format fragment '{${string}}'!`);
        }
    }
    AiracCycleFormatter.FORMAT_REGEXP = /({(?:(?:(?:eff|exp|expMinus)\((?:[^()]*)\))|[^{}]*)})/;
    AiracCycleFormatter.FRAGMENT_DATE_REGEXP = /(eff|exp|expMinus)\(([^()]*)\)/;
    AiracCycleFormatter.dateFormatterCache = new Map();

    /* eslint-disable jsdoc/check-indentation */
    /**
     * A utility class for creating degree-minute-second formatters for angle values.
     *
     * Each DMS formatter is a function which generates output strings from input angle values. The formatting behavior
     * of a formatter is defined by its format template.
     *
     * Format templates are strings which contain zero or more fragments enclosed by curly braces (`{}`); For a given
     * format template, an output string is generated from an input duration by replacing each fragment in the template
     * with a string generated from the input. The parts of the template string that are not contained in any fragment are
     * passed to the output unchanged. Each fragment defines how its replacement string is generated. There are two types
     * of fragments:
     * * Sign fragment. In EBNF notation, these take the form `['+', ['[', x, ']']], ('-' , ['[', y, ']'])`, where
     * `x` and `y` are arbitrary strings. Each sign fragment is replaced with a string representing the sign of the input.
     * The negative sign string is defined by `y`. If `y` is not defined, the negative sign string defaults to `'-'`
     * (dash). The positive sign string is defined by `x`. If the positive sign token does not appear in the fragment
     * definition, the positive sign string defaults to `''` (the empty string), otherwise it defaults to `'+'`.
     * * Numeric fragment. In EBNF notation, these take the form `{x}, ['?'], ['.', [{x}], ['(', {x}, ')']]`,
     * where `x = 'D' | 'M' | 'S' | 'd' | 'm' | 's'`. Each numeric fragment is replaced with the numeric value of the
     * duration in degrees, minutes, or seconds, depending on which character is used for `x`. With uppercase letters, the
     * entire portion of the input value is used. With lowercase letters, only the portion of the input value that does not
     * divide evenly into the next smallest unit is used (for hours, which is the largest unit, there is no difference
     * between using `'H'` and `'h'`).
     *   * The number of `x` characters to the left of the decimal point (including all characters if no decimal point is
     * present) in the definition controls the number of leading zeroes with which the output will be padded.
     *   * If the optional `'?'` character is present, the output will drop all digits to the left of the decimal point if
     * all such digits are equal to 0.
     *   * The total number of `x` characters to the right of the decimal point in the definition controls the decimal
     * precision of the output. Trailing zeroes to the right of the decimal point will be added to the output to a number
     * of decimal places equal to the number of non-parenthetical `x` characters to the right of the decimal point in the
     * definition. If there are no `x` characters to the right of the decimal point in the definition, then the output will
     * have infinite decimal precision with no extraneous trailing zeroes.
     *   * Rounding behavior is always round down.
     *
     * @example <caption>Formatting to degrees-minutes-seconds</caption>
     * const formatter = DmsFormatter2.create('{d}°{mm}\'{ss}"', UnitType.DEGREE);
     * formatter(10);         // 10°00'00"
     * formatter(10.51);      // 10°30'36"
     *
     * @example <caption>Formatting to degrees-minutes-seconds with decimal precision</caption>
     * const formatter = DmsFormatter2.create('{d}°{mm}\'{ss.s(s)}"', UnitType.DEGREE);
     * formatter(10);         // 10°00'00.0"
     * formatter(10.09169);   // 10°05'30.08"
     *
     * @example <caption>Formatting to degrees-minutes</caption>
     * const formatter = DmsFormatter2.create('{d}°{mm.mm}\'', UnitType.DEGREE);
     * formatter(10.09169);   // 10°05.50'
     *
     * @example <caption>Formatting with signs</caption>
     * const formatter = DmsFormatter2.create('{-}{d}°{mm}\'', UnitType.DEGREE);
     * formatter(10);                   // 10°00'
     * formatter(-10);                  // -10°00'
     *
     * const formatterWithPositiveSign = DmsFormatter2.create('{+-}{d}°{mm}\'', UnitType.DEGREE);
     * formatterWithPositiveSign(10);   // +10°00'
     *
     * const formatterWithRealMinusSign = DmsFormatter2.create('{-[–]}{d}°{mm}\'', UnitType.DEGREE);
     * formatterWithRealMinusSign(10);  // –10°00'
     */
    class DmsFormatter2 {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(format, unit, precision, arg4) {
            const builder = DmsFormatter2.createBuilder(format, precision, unit);
            const options = DmsFormatter2.resolveOptions(typeof arg4 === 'string' ? { nanString: arg4 } : arg4);
            const built = Array.from(builder, () => '');
            if (options.cache) {
                let cachedInput = undefined;
                let cachedOutput = undefined;
                return (angle) => {
                    if (isNaN(angle)) {
                        return options.nanString;
                    }
                    const roundedInput = MathUtils.round(angle, precision);
                    if (cachedInput !== undefined && cachedOutput !== undefined && roundedInput === cachedInput) {
                        return cachedOutput;
                    }
                    cachedInput = roundedInput;
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](angle, unit);
                    }
                    return cachedOutput = built.join('');
                };
            }
            else {
                return (angle) => {
                    if (isNaN(angle)) {
                        return options.nanString;
                    }
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](angle, unit);
                    }
                    return built.join('');
                };
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static createForNumberUnit(format, precision, arg3) {
            const builder = DmsFormatter2.createBuilder(format, precision.number, precision.unit);
            const options = DmsFormatter2.resolveOptions(typeof arg3 === 'string' ? { nanString: arg3 } : arg3);
            const built = Array.from(builder, () => '');
            if (options.cache) {
                let cachedInput = undefined;
                let cachedOutput = undefined;
                return (angle) => {
                    if (angle.isNaN()) {
                        return options.nanString;
                    }
                    const roundedInput = MathUtils.round(angle.asUnit(precision.unit), precision.number);
                    if (cachedInput !== undefined && cachedOutput !== undefined && roundedInput === cachedInput) {
                        return cachedOutput;
                    }
                    cachedInput = roundedInput;
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](angle.number, angle.unit);
                    }
                    return cachedOutput = built.join('');
                };
            }
            else {
                return (angle) => {
                    if (angle.isNaN()) {
                        return options.nanString;
                    }
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](angle.number, angle.unit);
                    }
                    return built.join('');
                };
            }
        }
        /**
         * Resolves a full set of options from a partial options object. Any option not explicitly defined by the partial
         * options object will revert to its default value.
         * @param options A partial options object.
         * @returns A new options object containing the full set of options resolved from the specified partial options
         * object.
         */
        static resolveOptions(options) {
            var _a;
            const resolved = Object.assign({}, options);
            for (const key in DmsFormatter2.DEFAULT_OPTIONS) {
                (_a = resolved[key]) !== null && _a !== void 0 ? _a : (resolved[key] = DmsFormatter2.DEFAULT_OPTIONS[key]);
            }
            return resolved;
        }
        /**
         * Creates an output string builder from a format template.
         * @param format A format template.
         * @param precision The desired precision.
         * @param precisionUnit The unit type of the desired precision.
         * @returns An output string builder which conforms to the specified format template.
         */
        static createBuilder(format, precision, precisionUnit) {
            const split = format.split(DmsFormatter2.FORMAT_REGEXP);
            return split.map((string) => {
                if (string.match(DmsFormatter2.FORMAT_REGEXP)) {
                    return DmsFormatter2.parseFragment(string.substring(1, string.length - 1), precision, precisionUnit);
                }
                else {
                    return () => string;
                }
            });
        }
        /**
         * Parses a format template fragment and returns a function which generates a string from an input angle according
         * to the rules defined by the fragment. If the fragment is malformed, this method returns a function which always
         * generates an empty string.
         * @param fragment A format template fragment definition.
         * @param precision The desired precision.
         * @param precisionUnit The unit type of the desired precision.
         * @returns A function which generates a string from an input angle in milliseconds according to the rules defined
         * by the template fragment.
         */
        static parseFragment(fragment, precision, precisionUnit) {
            var _a, _b;
            const signMatch = fragment.match(DmsFormatter2.SIGN_FRAGMENT_REGEX);
            if (signMatch) {
                const [, posCharMatch, // Matches the positive sign char ('+')
                posStringMatch, // Matches the string assigned to represent the positive sign
                negStringMatch // Matches the string assigned to represent the negative sign
                ] = signMatch;
                const posSign = posCharMatch === '+' ? posStringMatch !== null && posStringMatch !== void 0 ? posStringMatch : '+' : '';
                const negSign = negStringMatch !== null && negStringMatch !== void 0 ? negStringMatch : '-';
                return (angle) => {
                    return angle < 0 ? negSign : posSign;
                };
            }
            const numericMatch = fragment.match(DmsFormatter2.NUM_FRAGMENT_REGEXP);
            if (!numericMatch) {
                console.warn(`DmsFormatter2: discarding fragment due to invalid syntax: {${fragment}}`);
                return () => '';
            }
            const [, leftMatch, // Matches unit chars to the left of the decimal point
            unitMatch, // Matches the unit char ('D', 'M', 'S', etc)
            leftOptionalMatch, // Matches the question mark just to the left of the decimal point
            rightMatch, // Matches the decimal point and all chars to the right
            rightForcedMatch, // Matches unit chars to the right of the decimal point not surrounded by parens
            rightOptionalMatch // Matches unit chars to the right of the decimal point surrounded by parens
            ] = numericMatch;
            const unitInfo = DmsFormatter2.NUM_FRAGMENT_UNIT_INFO[unitMatch];
            const pad = leftMatch.length;
            const dropZero = !!leftOptionalMatch;
            const step = precisionUnit.convertTo(precision, unitInfo.unit);
            const convertFunc = step <= 0
                ? ((angle, unit) => {
                    return unitInfo.unit.convertFrom(Math.abs(angle), unit) % unitInfo.mod;
                }) : ((angle, unit, epsilon) => {
                return (MathUtils.round(unitInfo.unit.convertFrom(Math.abs(angle), unit), step) + epsilon) % unitInfo.mod;
            });
            const formatLeftFunc = dropZero
                ? ((input) => {
                    const rounded = Math.floor(input);
                    return rounded === 0 ? '' : rounded.toString().padStart(pad, '0');
                }) : ((input) => Math.floor(input).toString().padStart(pad, '0'));
            if (rightMatch) {
                if (rightMatch.length === 1) {
                    // Unlimited decimal precision
                    return (angle, unit) => {
                        const converted = unitInfo.unit.convertFrom(Math.abs(angle), unit) % unitInfo.mod;
                        const decimal = converted % 1;
                        return `${formatLeftFunc(converted)}${decimal.toString().substring(1)}`;
                    };
                }
                const forcedDecimalPlaces = (_a = rightForcedMatch === null || rightForcedMatch === void 0 ? void 0 : rightForcedMatch.length) !== null && _a !== void 0 ? _a : 0;
                const unforcedDecimalPlaces = (_b = rightOptionalMatch === null || rightOptionalMatch === void 0 ? void 0 : rightOptionalMatch.length) !== null && _b !== void 0 ? _b : 0;
                const totalDecimalPlaces = forcedDecimalPlaces + unforcedDecimalPlaces;
                const factor = Math.pow(10, totalDecimalPlaces);
                const epsilon = Math.min(step / 2, 1 / (2 * factor));
                return (angle, unit) => {
                    const converted = convertFunc(angle, unit, epsilon);
                    const decimal = converted % 1;
                    const decimalRounded = Math.floor(decimal * factor) / factor;
                    return `${formatLeftFunc(converted)}.${decimalRounded.toString().substring(2).padEnd(forcedDecimalPlaces, '0')}`;
                };
            }
            else {
                const epsilon = Math.min(step / 2, 0.5);
                return (angle, unit) => {
                    return formatLeftFunc(convertFunc(angle, unit, epsilon));
                };
            }
        }
    }
    DmsFormatter2.FORMAT_REGEXP = /({[^{}]*})/;
    DmsFormatter2.SIGN_FRAGMENT_REGEX = /^(?:(\+)(?:\[(.*)\])?)?-(?:\[(.*)\])?$/;
    DmsFormatter2.NUM_FRAGMENT_REGEXP = /^(([DMSdms])\2*)(\?)?(?:(\.(\2*)(?:\((\2+)\))?)?)$/;
    DmsFormatter2.NUM_FRAGMENT_UNIT_INFO = {
        ['d']: { unit: UnitType.DEGREE, mod: Infinity },
        ['m']: { unit: UnitType.ARC_MIN, mod: 60 },
        ['s']: { unit: UnitType.ARC_SEC, mod: 60 },
        ['D']: { unit: UnitType.DEGREE, mod: Infinity },
        ['M']: { unit: UnitType.ARC_MIN, mod: Infinity },
        ['S']: { unit: UnitType.ARC_SEC, mod: Infinity }
    };
    /** The default options for degree-minute-second formatters. */
    DmsFormatter2.DEFAULT_OPTIONS = {
        nanString: 'NaN',
        cache: false
    };

    /* eslint-disable jsdoc/check-indentation */
    /**
     * A utility class for creating duration formatters.
     *
     * Each duration formatter is a function which generates output strings from input duration values. The formatting
     * behavior of a formatter is defined by its format template.
     *
     * Format templates are strings which contain zero or more fragments enclosed by curly braces (`{}`); For a given
     * format template, an output string is generated from an input duration by replacing each fragment in the template
     * with a string generated from the input. The parts of the template string that are not contained in any fragment are
     * passed to the output unchanged. Each fragment defines how its replacement string is generated. There are two types
     * of fragments:
     * * Sign fragment. In EBNF notation, these take the form `['+', ['[', x, ']']], ('-' , ['[', y, ']'])`, where
     * `x` and `y` are arbitrary strings. Each sign fragment is replaced with a string representing the sign of the input.
     * The negative sign string is defined by `y`. If `y` is not defined, the negative sign string defaults to `'-'`
     * (dash). The positive sign string is defined by `x`. If the positive sign token does not appear in the fragment
     * definition, the positive sign string defaults to `''` (the empty string), otherwise it defaults to `'+'`.
     * * Numeric fragment. In EBNF notation, these take the form
     * `{x}, ['?'], ['.', [{x}], ['(', {x}, ')']]`, where `x = 'H' | 'M' | 'S' | 'h' | 'm' | 's'`. Each numeric fragment is
     * replaced with the numeric value of the duration in hours, minutes, or seconds, depending on which character is used
     * for `x`. With uppercase letters, the entire portion of the input value is used. With lowercase letters, only the
     * portion of the input value that does not divide evenly into the next smallest unit is used (for hours, which is the
     * largest unit, there is no difference between using `'H'` and `'h'`).
     *   * The number of `x` characters to the left of the decimal point (including all characters if no decimal point is
     * present) in the definition controls the number of leading zeroes with which the output will be padded.
     *   * If the optional `'?'` character is present, the output will drop all digits to the left of the decimal point if
     * all such digits are equal to 0.
     *   * The total number of `x` characters to the right of the decimal point in the definition controls the decimal
     * precision of the output. Trailing zeroes to the right of the decimal point will be added to the output to a number
     * of decimal places equal to the number of non-parenthetical `x` characters to the right of the decimal point in the
     * definition. If there are no `x` characters to the right of the decimal point in the definition, then the output will
     * have infinite decimal precision with no extraneous trailing zeroes.
     *   * Rounding behavior is always round down.
     *
     * @example <caption>Formatting to hours-minutes-seconds</caption>
     * const formatter = DurationFormatter.create('{h}:{mm}:{ss}', UnitType.SECOND);
     * formatter(3616);       // 1:00:16
     * formatter(36016.9);    // 10:00:16
     *
     * @example <caption>Formatting to hours-minutes-seconds with decimal precision</caption>
     * const formatter = DurationFormatter.create('{h}:{mm}:{ss.s(s)}', UnitType.SECOND);
     * formatter(3600);       // 1:00:00.0
     * formatter(3600.55);    // 1:00:00.55
     *
     * @example <caption>Formatting to minutes-seconds</caption>
     * const formatter = DurationFormatter.create('{MM}:{ss}', UnitType.SECOND);
     * formatter(600);        // 10:00
     * formatter(4200);       // 70:00
     *
     * @example <caption>Formatting with signs</caption>
     * const formatter = DurationFormatter.create('{-}{h}:{mm}', UnitType.SECOND);
     * formatter(3600);                   // 1:00
     * formatter(-3600);                  // -1:00
     *
     * const formatterWithPositiveSign = DurationFormatter.create('{+-}{h}:{mm}', UnitType.SECOND);
     * formatterWithPositiveSign(3600);   // +1:00
     *
     * const formatterWithRealMinusSign = DurationFormatter.create('{-[–]}{h}:{mm}', UnitType.SECOND);
     * formatterWithRealMinusSign(3600);  // –1:00
     */
    class DurationFormatter {
        // eslint-disable-next-line jsdoc/require-jsdoc
        static create(format, unit, precision, arg4) {
            const builder = DurationFormatter.createBuilder(format, precision, unit);
            const options = DurationFormatter.resolveOptions(typeof arg4 === 'string' ? { nanString: arg4 } : arg4);
            const built = Array.from(builder, () => '');
            if (options.cache) {
                let cachedInput = undefined;
                let cachedOutput = undefined;
                return (duration) => {
                    if (isNaN(duration)) {
                        return options.nanString;
                    }
                    const roundedInput = MathUtils.round(duration, precision);
                    if (cachedInput !== undefined && cachedOutput !== undefined && roundedInput === cachedInput) {
                        return cachedOutput;
                    }
                    cachedInput = roundedInput;
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](duration, unit);
                    }
                    return cachedOutput = built.join('');
                };
            }
            else {
                return (duration) => {
                    if (isNaN(duration)) {
                        return options.nanString;
                    }
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](duration, unit);
                    }
                    return built.join('');
                };
            }
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        static createForNumberUnit(format, precision, arg3) {
            const builder = DurationFormatter.createBuilder(format, precision.number, precision.unit);
            const options = DurationFormatter.resolveOptions(typeof arg3 === 'string' ? { nanString: arg3 } : arg3);
            const built = Array.from(builder, () => '');
            if (options.cache) {
                let cachedInput = undefined;
                let cachedOutput = undefined;
                return (duration) => {
                    if (duration.isNaN()) {
                        return options.nanString;
                    }
                    const roundedInput = MathUtils.round(duration.asUnit(precision.unit), precision.number);
                    if (cachedInput !== undefined && cachedOutput !== undefined && roundedInput === cachedInput) {
                        return cachedOutput;
                    }
                    cachedInput = roundedInput;
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](duration.number, duration.unit);
                    }
                    return cachedOutput = built.join('');
                };
            }
            else {
                return (duration) => {
                    if (duration.isNaN()) {
                        return options.nanString;
                    }
                    for (let i = 0; i < builder.length; i++) {
                        built[i] = builder[i](duration.number, duration.unit);
                    }
                    return built.join('');
                };
            }
        }
        /**
         * Resolves a full set of options from a partial options object. Any option not explicitly defined by the partial
         * options object will revert to its default value.
         * @param options A partial options object.
         * @returns A new options object containing the full set of options resolved from the specified partial options
         * object.
         */
        static resolveOptions(options) {
            var _a;
            const resolved = Object.assign({}, options);
            for (const key in DurationFormatter.DEFAULT_OPTIONS) {
                (_a = resolved[key]) !== null && _a !== void 0 ? _a : (resolved[key] = DurationFormatter.DEFAULT_OPTIONS[key]);
            }
            return resolved;
        }
        /**
         * Creates an output string builder from a format template.
         * @param format A format template.
         * @param precision The desired precision.
         * @param precisionUnit The unit type of the desired precision.
         * @returns An output string builder which conforms to the specified format template.
         */
        static createBuilder(format, precision, precisionUnit) {
            const split = format.split(DurationFormatter.FORMAT_REGEXP);
            return split.map((string) => {
                if (string.match(DurationFormatter.FORMAT_REGEXP)) {
                    return DurationFormatter.parseFragment(string.substring(1, string.length - 1), precision, precisionUnit);
                }
                else {
                    return () => string;
                }
            });
        }
        /**
         * Parses a format template fragment and returns a function which generates a string from an input duration according
         * to the rules defined by the fragment. If the fragment is malformed, this method returns a function which always
         * generates an empty string.
         * @param fragment A format template fragment definition.
         * @param precision The desired precision.
         * @param precisionUnit The unit type of the desired precision.
         * @returns A function which generates a string from an input duration in milliseconds according to the rules defined
         * by the template fragment.
         */
        static parseFragment(fragment, precision, precisionUnit) {
            var _a, _b;
            const signMatch = fragment.match(DurationFormatter.SIGN_FRAGMENT_REGEX);
            if (signMatch) {
                const [, posCharMatch, // Matches the positive sign char ('+')
                posStringMatch, // Matches the string assigned to represent the positive sign
                negStringMatch // Matches the string assigned to represent the negative sign
                ] = signMatch;
                const posSign = posCharMatch === '+' ? posStringMatch !== null && posStringMatch !== void 0 ? posStringMatch : '+' : '';
                const negSign = negStringMatch !== null && negStringMatch !== void 0 ? negStringMatch : '-';
                return (angle) => {
                    return angle < 0 ? negSign : posSign;
                };
            }
            const numericMatch = fragment.match(DurationFormatter.NUM_FRAGMENT_REGEXP);
            if (!numericMatch) {
                console.warn(`DurationFormatter: discarding fragment due to invalid syntax: {${fragment}}`);
                return () => '';
            }
            const [, leftMatch, // Matches unit chars to the left of the decimal point
            unitMatch, // Matches the unit char ('H', 'M', 'S', etc)
            leftOptionalMatch, // Matches the question mark just to the left of the decimal point
            rightMatch, // Matches the decimal point and all chars to the right
            rightForcedMatch, // Matches unit chars to the right of the decimal point not surrounded by parens
            rightOptionalMatch // Matches unit chars to the right of the decimal point surrounded by parens
            ] = numericMatch;
            const unitInfo = DurationFormatter.NUM_FRAGMENT_UNIT_INFO[unitMatch];
            const pad = leftMatch.length;
            const dropZero = !!leftOptionalMatch;
            const step = precisionUnit.convertTo(precision, unitInfo.unit);
            const convertFunc = step <= 0
                ? ((duration, unit) => {
                    return unitInfo.unit.convertFrom(Math.abs(duration), unit) % unitInfo.mod;
                }) : ((duration, unit, epsilon) => {
                return (MathUtils.round(unitInfo.unit.convertFrom(Math.abs(duration), unit), step) + epsilon) % unitInfo.mod;
            });
            const formatLeftFunc = dropZero
                ? ((input) => {
                    const rounded = Math.floor(input);
                    return rounded === 0 ? '' : rounded.toString().padStart(pad, '0');
                })
                : ((input) => Math.floor(input).toString().padStart(pad, '0'));
            if (rightMatch) {
                if (rightMatch.length === 1) {
                    // Unlimited decimal precision
                    return (duration, unit) => {
                        const converted = unitInfo.unit.convertFrom(Math.abs(duration), unit) % unitInfo.mod;
                        const decimal = converted % 1;
                        return `${formatLeftFunc(converted)}${decimal.toString().substring(1)}`;
                    };
                }
                const forcedDecimalPlaces = (_a = rightForcedMatch === null || rightForcedMatch === void 0 ? void 0 : rightForcedMatch.length) !== null && _a !== void 0 ? _a : 0;
                const unforcedDecimalPlaces = (_b = rightOptionalMatch === null || rightOptionalMatch === void 0 ? void 0 : rightOptionalMatch.length) !== null && _b !== void 0 ? _b : 0;
                const totalDecimalPlaces = forcedDecimalPlaces + unforcedDecimalPlaces;
                const factor = Math.pow(10, totalDecimalPlaces);
                const epsilon = Math.min(step / 2, 1 / (2 * factor));
                return (duration, unit) => {
                    const converted = convertFunc(duration, unit, epsilon);
                    const decimal = converted % 1;
                    const decimalRounded = Math.floor(decimal * factor) / factor;
                    return `${formatLeftFunc(converted)}.${decimalRounded.toString().substring(2).padEnd(forcedDecimalPlaces, '0')}`;
                };
            }
            else {
                const epsilon = Math.min(step / 2, 0.5);
                return (duration, unit) => {
                    return formatLeftFunc(convertFunc(duration, unit, epsilon));
                };
            }
        }
    }
    DurationFormatter.FORMAT_REGEXP = /({[^{}]*})/;
    DurationFormatter.SIGN_FRAGMENT_REGEX = /^(?:(\+)(?:\[(.*)\])?)?-(?:\[(.*)\])?$/;
    DurationFormatter.NUM_FRAGMENT_REGEXP = /^(([HMShms])\2*)(\?)?(?:(\.(\2*)(?:\((\2+)\))?)?)$/;
    DurationFormatter.NUM_FRAGMENT_UNIT_INFO = {
        ['h']: { unit: UnitType.HOUR, mod: Infinity },
        ['m']: { unit: UnitType.MINUTE, mod: 60 },
        ['s']: { unit: UnitType.SECOND, mod: 60 },
        ['H']: { unit: UnitType.HOUR, mod: Infinity },
        ['M']: { unit: UnitType.MINUTE, mod: Infinity },
        ['S']: { unit: UnitType.SECOND, mod: Infinity }
    };
    /** The default options for duration formatters. */
    DurationFormatter.DEFAULT_OPTIONS = {
        nanString: 'NaN',
        cache: false
    };

    /**
     * A utility class for creating radio frequency formatters.
     */
    class RadioFrequencyFormatter {
        /**
         * Creates a function which formats NAV radio frequencies in hertz. The formatted string created by the function
         * displays the frequency in megahertz to two decimal places.
         * @param nanString The string to output for an input of `NaN`. Defaults to `'NaN'`.
         * @returns A function which formats NAV radio frequencies in hertz.
         */
        static createNav(nanString = 'NaN') {
            return (freqHz) => {
                return isNaN(freqHz) ? nanString : (freqHz / 1e6).toFixed(2);
            };
        }
        /**
         * Creates a function which formats COM radio frequencies in hertz. The formatted string created by the function
         * displays the frequency in megahertz to either two decimal places (with the third decimal place truncated) for
         * 25 kHz spacing, or three decimal places for 8.33 kHz spacing.
         * @param spacing The COM channel spacing mode to use.
         * @param nanString The string to output for an input of `NaN`. Defaults to `'NaN'`.
         * @returns A function which formats COM radio frequencies in hertz.
         */
        static createCom(spacing, nanString = 'NaN') {
            if (spacing === exports.ComSpacing.Spacing833Khz) {
                return (freqHz) => {
                    return isNaN(freqHz) ? nanString : (freqHz / 1e6).toFixed(3);
                };
            }
            else {
                return (freqHz) => {
                    return isNaN(freqHz) ? nanString : (freqHz / 1e6).toFixed(3).slice(0, -1);
                };
            }
        }
        /**
         * Creates a function which formats ADF radio frequencies in hertz. The formatted string created by the function
         * displays the frequency in kilohertz to one decimal place.
         * @param nanString The string to output for an input of `NaN`. Defaults to `'NaN'`.
         * @returns A function which formats ADF radio frequencies in hertz.
         */
        static createAdf(nanString = 'NaN') {
            return (freqHz) => {
                return isNaN(freqHz) ? nanString : (freqHz / 1e3).toFixed(1);
            };
        }
    }

    /**
     * Utils for performance plans
     */
    class PerformancePlanUtils {
        /**
         * Serializes a plan
         *
         * @param plan the plan to serialize
         *
         * @returns the serialized JSON string
         */
        static serialize(plan) {
            const tmpObj = {};
            Object.keys(plan).forEach((key) => {
                tmpObj[key] = plan[key].get();
            });
            return JSON.stringify(tmpObj);
        }
        /**
         * Deserializes a serialized performance plan into a plan
         *
         * @param data the serialized data string
         * @param plan the plan to deserialize into
         */
        static deserializeInto(data, plan) {
            const customData = JSON.parse(data);
            Object.keys(customData).forEach((key) => {
                const value = customData[key];
                if (value !== undefined) {
                    plan[key].set(value);
                }
            });
        }
    }

    /**
     * Proxied `MutableSubscribable` that mirrors a property to be accessed by {@link PerformancePlanProxy}
     */
    class ProxiedPerformancePlanProperty extends AbstractSubscribable {
        /**
         * Ctor
         *
         * @param proxy the proxy this property belongs to
         * @param key property key
         * @param differentiateBetweenFlightPlans whether the property, when edited, should be differentiated between flight plans
         */
        constructor(proxy, key, differentiateBetweenFlightPlans) {
            super();
            this.proxy = proxy;
            this.key = key;
            this.differentiateBetweenFlightPlans = differentiateBetweenFlightPlans;
            this.isSubscribable = true;
            this.isMutableSubscribable = true;
            this.targetPlan = null;
            this.subject = Subject.create(this.proxy.defaultValuesPlan[this.key].get());
            this.backSubjectSubscription = undefined;
        }
        /**
         * Switches the target plan
         *
         * @param plan the new target plan
         */
        switchToPlan(plan) {
            var _a;
            this.targetPlan = plan;
            (_a = this.backSubjectSubscription) === null || _a === void 0 ? void 0 : _a.destroy();
            this.backSubjectSubscription = this.backingSubject().sub((it) => {
                this.subject.set(it);
            }, true);
        }
        /**
         * Returns the backing subject in the target plan for the property
         *
         * @returns the subject
         *
         * @throws if no target plan exists
         */
        backingSubject() {
            if (!this.targetPlan) {
                throw new Error('No current target plan');
            }
            return this.targetPlan[this.key];
        }
        /**
         * Resets the property to its default value according to the default values performance plan
         */
        resetToDefault() {
            const defaultValue = this.proxy.defaultValuesPlan[this.key];
            this.proxy.onBeforeEdit(this, defaultValue);
            this.backingSubject().set(defaultValue.get());
            this.proxy.onAfterEdit(this, defaultValue);
        }
        /** @inheritDoc */
        get() {
            if (!this.targetPlan) {
                throw new Error('No current target plan');
            }
            return this.backingSubject().get();
        }
        /** @inheritDoc */
        set(value) {
            if (!this.targetPlan) {
                throw new Error('No current target plan');
            }
            this.proxy.onBeforeEdit(this, value);
            this.backingSubject().set(value);
            this.proxy.onAfterEdit(this, value);
        }
        /** @inheritDoc */
        sub(handler, initialNotify, paused) {
            return this.subject.sub(handler, initialNotify, paused);
        }
    }

    /**
     * Correlates flight plan indices with performance plan objects
     */
    class PerformancePlanRepository {
        /**
         * Ctor
         *
         * @param id the performance plan repository ID
         * @param bus the event bus
         * @param flightPlanner a flight planner instance
         * @param definitions an object containing definitions for each of the performance plan's properties
         */
        constructor(id, bus, flightPlanner, definitions) {
            this.id = id;
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.definitions = definitions;
            this.repoId = Math.floor(Math.random() * 10000000);
            this.eventSuffix = (this.id === '' ? '' : `_${this.id}`);
            this._plans = [];
            this.plans = this._plans;
            this._plans[PerformancePlanRepository.DEFAULT_VALUES_PLAN_INDEX] = this.createPlanFromDefinitions();
            this._plans[PerformancePlanRepository.SYNC_PLAN_INDEX] = this.createPlanFromDefinitions();
            this._plans[flightPlanner.activePlanIndex] = this.createPlanFromDefinitions();
            const sub = this.bus.getSubscriber();
            sub.on(`performancePlanChanged${this.eventSuffix}`).handle((data) => {
                if (data.repoId !== this.repoId) {
                    PerformancePlanUtils.deserializeInto(data.serializedPlan, this._plans[PerformancePlanRepository.SYNC_PLAN_INDEX]);
                    if (this.has(data.planIndex)) {
                        this.copy(PerformancePlanRepository.SYNC_PLAN_INDEX, data.planIndex, true, false);
                    }
                }
            });
        }
        /**
         * Whether the repository has a performance plan already stored for a given index
         *
         * @param index the index
         *
         * @returns boolean
         */
        has(index) {
            return this._plans[index] !== undefined;
        }
        /**
         * Returns a performance plan for a given flight plan index, or creates it
         *
         * @param index flight plan index
         *
         * @throws if an invalid flight plan index is specified
         *
         * @returns the performance plan
         */
        forFlightPlanIndex(index) {
            const existing = this._plans[index];
            if (!existing) {
                return this.create(index);
            }
            return existing;
        }
        /**
         * Gets the active plan. This is the plan that always exists even if there is no valid flight plan yet.
         * @returns the active plan
         */
        getActivePlan() {
            return this._plans[this.flightPlanner.activePlanIndex];
        }
        /**
         * Returns the performance plan containing default values
         *
         * @returns the plan
         */
        defaultValuesPlan() {
            return this._plans[PerformancePlanRepository.DEFAULT_VALUES_PLAN_INDEX];
        }
        /**
         * Returns whether this plan repository has nay valid plans
         *
         * @returns boolean
         */
        hasAnyPlan() {
            return this._plans.some((it, index) => !!it && this.flightPlanner.hasFlightPlan(index));
        }
        /**
         * Creates a performance plan at the given index if it doesn't exist, or returns the existing one
         *
         * @param atIndex the index
         *
         * @returns the created plan
         */
        create(atIndex) {
            if (!this.has(atIndex)) {
                const newPerformancePlan = this.createPlanFromDefinitions();
                this._plans[atIndex] = newPerformancePlan;
                return newPerformancePlan;
            }
            else {
                return this._plans[atIndex];
            }
        }
        /**
         * Copies a performance plan onto another
         *
         * @param from from index
         * @param to to index
         * @param skipChecks whether to skip flight planner checks
         * @param sync whether to trigger a perf plan sync
         */
        copy(from, to, skipChecks = false, sync = true) {
            const fromPlan = this._plans[from];
            const toPlan = skipChecks ? this._plans[to] : this.forFlightPlanIndex(to);
            // Copy data
            for (const key in toPlan) {
                const fromValue = fromPlan[key];
                const toValue = toPlan[key];
                if (fromValue instanceof Subject && toValue instanceof Subject) {
                    // do a shallow comparison of properties if fromValue is a complex type
                    if (typeof fromValue.get() === 'object' && this.isSameObject(fromValue.get(), toValue.get())) {
                        continue;
                    }
                    toValue.set(fromValue.get());
                }
            }
            sync && this.triggerSync(to);
        }
        /**
         * Checks whether two objects are the same by performing a (very) shallow comparison.
         * @param obj1 The first object
         * @param obj2 The second object
         * @returns true if the objects are the same
         */
        isSameObject(obj1, obj2) {
            // Check if the values are null or undefined
            if (obj1 == null || obj2 == null) {
                return obj1 === obj2;
            }
            for (const prop in obj1) {
                if (typeof obj1[prop] === 'object') {
                    continue;
                }
                if (obj1[prop] !== obj2[prop]) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Triggers a synchronisation of the active plan performance plan over the EventBus.
         * @param planIndex the plan index
         */
        triggerSync(planIndex) {
            const packet = {
                repoId: this.repoId,
                planIndex,
                serializedPlan: PerformancePlanUtils.serialize(this._plans[planIndex]),
            };
            this.bus.getPublisher().pub(`performancePlanChanged${this.eventSuffix}`, packet, true, true);
        }
        /**
         * Creates a performance plan from the definitions object
         *
         * @returns a performance plan with default values filled
         */
        createPlanFromDefinitions() {
            const plan = {};
            for (const [key, definition] of Object.entries(this.definitions)) {
                plan[key] = Subject.create(definition.defaultValue);
            }
            return plan;
        }
        /**
         * Creates a performance plan proxy from the definitions object
         *
         * @param callbacks an object containing the callbacks to attach to the proxy
         *
         * @returns a performance plan proxy
         */
        createPerformancePlanProxy(callbacks) {
            var _a;
            const defaultValuesPlan = this.defaultValuesPlan();
            const definitions = this.definitions;
            const proxy = {
                defaultValuesPlan,
                /** @inheritDoc */
                switchToPlan(plan, initial) {
                    for (const [key, definition] of Object.entries(definitions)) {
                        if (initial || definition.differentiateBetweenFlightPlans) {
                            const typedKey = key;
                            this[typedKey].switchToPlan(plan);
                        }
                    }
                },
                /** @inheritDoc */
                onBeforeEdit(property, newValue) {
                    callbacks.onBeforeEdit(property, newValue);
                },
                /** @inheritDoc */
                onAfterEdit(property, newValue) {
                    callbacks.onAfterEdit(property, newValue);
                },
            };
            for (const [key, definition] of Object.entries(definitions)) {
                const typedKey = key;
                const typedDefinition = definition;
                const property = new ProxiedPerformancePlanProperty(proxy, typedKey, (_a = typedDefinition.differentiateBetweenFlightPlans) !== null && _a !== void 0 ? _a : false);
                proxy[typedKey] = property;
            }
            proxy.switchToPlan(this.getActivePlan(), true);
            for (const [key, definition] of Object.entries(definitions)) {
                const typedKey = key;
                const typedDefinition = definition;
                proxy[typedKey].set(typedDefinition.defaultValue);
            }
            return proxy;
        }
    }
    PerformancePlanRepository.DEFAULT_VALUES_PLAN_INDEX = Number.MAX_SAFE_INTEGER;
    PerformancePlanRepository.SYNC_PLAN_INDEX = PerformancePlanRepository.DEFAULT_VALUES_PLAN_INDEX - 1;

    /**
     * A manager for user settings. Provides settings using their names as keys, publishes value change events on the
     * event bus, and keeps setting values up to date when receiving change events across the bus.
     */
    class DefaultUserSettingManager {
        /**
         * Constructor.
         * @param bus The bus used by this manager to publish setting change events.
         * @param settingDefs The setting definitions used to initialize this manager's settings.
         * @param keepLocal If present and true, values will be kept local to the instrument on which they're set.
         */
        constructor(bus, settingDefs, keepLocal = false) {
            this.bus = bus;
            this.publisher = this.bus.getPublisher();
            this.subscriber = this.bus.getSubscriber();
            this.syncPublisher = this.bus.getPublisher();
            this.syncSubscriber = this.bus.getSubscriber();
            this.keepLocal = keepLocal;
            this.settings = new Map(settingDefs.map(def => {
                const initTopic = `usersetting_init_${def.name}`;
                const syncTopic = `usersetting_sync_${def.name}`;
                const entry = {
                    syncTopic,
                    syncTime: 0,
                    initUid: Math.round(Math.random() * Number.MAX_SAFE_INTEGER)
                };
                entry.setting = new SyncableUserSetting(def, this.onSettingValueChanged.bind(this, entry));
                entry.initSub = this.syncSubscriber.on(initTopic).handle(data => {
                    // Do not respond to our own initialization sync.
                    if (data.uid === entry.initUid) {
                        return;
                    }
                    // If we receive an initialization sync event for a setting, that means a manager on another instrument tried
                    // to initialize the same setting to its default value. However, since the setting already exists here, we will
                    // send a response to override the initialized value with the existing value.
                    this.syncPublisher.pub(entry.syncTopic, { value: entry.setting.value, syncTime: entry.syncTime, initUid: data.uid }, !this.keepLocal, true);
                }, true);
                // Because sync events are cached, the initial subscriptions to the sync topic below will grab the synced value
                // of the new setting if it exists on the local instrument (e.g. if the value was synced from another instrument
                // after the local instrument was created but before this manager and local setting were created).
                this.syncSubscriber.on(syncTopic).handle(this.onSettingValueSynced.bind(this, entry));
                if (entry.syncTime === 0) {
                    // If the new setting has no synced value on the local instrument, we will try to grab an initialization value
                    // instead. If one exists, we will use it, but keep the local sync time at 0. If there is a pending response
                    // to this initialization value, we want to be ready to accept the response when it arrives, which we can't do
                    // if the local sync time is non-zero).
                    const sub = this.syncSubscriber.on(initTopic).handle(data => {
                        this.onSettingValueSynced(entry, { value: data.value, syncTime: 0 });
                    });
                    sub.destroy();
                }
                if (entry.syncTime === 0) {
                    // An existing synced value does not exist for the new setting on the local instrument, so we will go ahead
                    // and initialize the new setting value to its default and send an initialization sync event. If the setting
                    // exists on other instruments, their managers will send an initialization response to override our initialized
                    // value.
                    this.syncPublisher.pub(initTopic, { value: entry.setting.value, syncTime: Date.now(), uid: entry.initUid }, !this.keepLocal, true);
                    this.publisher.pub(entry.setting.definition.name, entry.setting.value, false, true);
                }
                entry.initSub.resume();
                return [def.name, entry];
            }));
        }
        /** @inheritdoc */
        tryGetSetting(name) {
            var _a;
            return (_a = this.settings.get(name)) === null || _a === void 0 ? void 0 : _a.setting;
        }
        /** @inheritdoc */
        getSetting(name) {
            const setting = this.tryGetSetting(name);
            if (setting === undefined) {
                throw new Error(`DefaultUserSettingManager: Could not find setting with name ${name}`);
            }
            return setting;
        }
        /** @inheritdoc */
        getAllSettings() {
            return Array.from(this.settings.values(), entry => entry.setting);
        }
        /** @inheritdoc */
        whenSettingChanged(name) {
            const setting = this.settings.get(name);
            if (!setting) {
                throw new Error(`DefaultUserSettingManager: Could not find setting with name ${name}`);
            }
            return this.subscriber.on(name).whenChanged();
        }
        /** @inheritdoc */
        mapTo(map) {
            return new MappedUserSettingManager(this, map);
        }
        /**
         * A callback which is called when one of this manager's settings has its value changed locally.
         * @param entry The entry for the setting that was changed.
         * @param value The new value of the setting.
         */
        onSettingValueChanged(entry, value) {
            entry.syncTime = Date.now();
            this.syncPublisher.pub(entry.syncTopic, { value, syncTime: entry.syncTime }, !this.keepLocal, true);
        }
        /**
         * A callback which is called when a setting changed event is received over the event bus.
         * @param entry The entry for the setting that was changed.
         * @param data The sync data.
         */
        onSettingValueSynced(entry, data) {
            // If the sync event is an initialization response, ignore it if the local setting value has already been synced.
            // Otherwise, protect against race conditions by not responding to sync events older than the last time this
            // manager synced the setting.
            if ((data.initUid !== undefined && entry.syncTime !== 0)
                || (data.initUid === undefined && data.syncTime < entry.syncTime)) {
                return;
            }
            this.syncSettingFromEvent(entry, data);
        }
        /**
         * Syncs a setting using data received from a sync event.
         * @param entry The entry for the setting to sync.
         * @param data The sync event data.
         */
        syncSettingFromEvent(entry, data) {
            entry.syncTime = data.syncTime;
            entry.setting.syncValue(data.value);
            // Publish the public setting change event. Do NOT sync across the bus because doing so can result in older events
            // being received after newer events.
            this.publisher.pub(entry.setting.definition.name, entry.setting.value, false, true);
        }
    }
    /**
     * A manager for user settings. Provides settings using their names as keys, publishes value change events on the
     * event bus, and keeps setting values up to date when receiving change events across the bus, using a mapping from
     * abstracted settings keys to true underlying settings keys.
     */
    class MappedUserSettingManager {
        /**
         * Creates an instance of a MappedUserSettingManager.
         * @param parent The parent setting manager.
         * @param map The map of abstracted keys to true underlying keys.
         */
        constructor(parent, map) {
            this.parent = parent;
            this.map = map;
        }
        /** @inheritdoc */
        tryGetSetting(name) {
            var _a;
            const mappedName = ((_a = this.map[name]) !== null && _a !== void 0 ? _a : name);
            return this.parent.tryGetSetting(mappedName);
        }
        /** @inheritdoc */
        getSetting(name) {
            var _a;
            const mappedName = ((_a = this.map[name]) !== null && _a !== void 0 ? _a : name);
            return this.parent.getSetting(mappedName);
        }
        /** @inheritdoc */
        whenSettingChanged(name) {
            var _a;
            const mappedName = ((_a = this.map[name]) !== null && _a !== void 0 ? _a : name);
            return this.parent.whenSettingChanged(mappedName);
        }
        /** @inheritdoc */
        getAllSettings() {
            return this.parent.getAllSettings();
        }
        /** @inheritdoc */
        mapTo(map) {
            return new MappedUserSettingManager(this, map);
        }
    }
    /**
     * An implementation of a user setting which can be synced across multiple instances.
     */
    class SyncableUserSetting extends AbstractSubscribable {
        // eslint-disable-next-line jsdoc/require-returns
        /** This setting's current value. */
        get value() {
            return this._value;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set value(v) {
            if (this._value === v) {
                return;
            }
            this._value = v;
            this.valueChangedCallback(v);
            this.notify();
        }
        /**
         * Constructor.
         * @param definition This setting's definition.
         * @param valueChangedCallback A function to be called whenever the value of this setting changes.
         */
        constructor(definition, valueChangedCallback) {
            super();
            this.definition = definition;
            this.valueChangedCallback = valueChangedCallback;
            this.isMutableSubscribable = true;
            this._value = definition.defaultValue;
        }
        /**
         * Syncs this setting to a value. This will not trigger a call to valueChangedCallback.
         * @param value The value to which to sync.
         */
        syncValue(value) {
            if (this._value === value) {
                return;
            }
            this._value = value;
            this.notify();
        }
        /** @inheritdoc */
        get() {
            return this._value;
        }
        /**
         * Sets the value of this setting.
         * @param value The new value.
         */
        set(value) {
            this.value = value;
        }
        /** @inheritdoc */
        resetToDefault() {
            this.set(this.definition.defaultValue);
        }
    }

    /**
     * An aliased user setting manager which can dynamically (re)define the settings from which its aliased settings are
     * sourced.
     */
    class AliasedUserSettingManager {
        /**
         * Constructor.
         * @param bus The bus used by this manager to publish setting change events.
         * @param settingDefs The setting definitions used to initialize this manager's settings. The definitions should
         * define the settings' aliased names.
         */
        constructor(bus, settingDefs) {
            this.bus = bus;
            this.aliasedSettings = new Map(settingDefs.map(def => [def.name, new AliasedUserSetting(def)]));
        }
        /**
         * Defines the mappings from this manager's aliased settings to their source settings. Once the mappings are defined,
         * each aliased setting will take the value of its source setting, and setting the value of the aliased setting will
         * also set the value of the source setting. If a source setting cannot be defined for an aliased setting, the
         * aliased setting's value will be fixed to its default value and cannot be changed.
         * @param masterManager The manager hosting the settings from which this manager's aliased settings will be sourced.
         * @param map The mappings for this manager's aliased settings, as a set of key-value pairs where the keys are the
         * aliased setting names and the values are the source setting names. For any aliased setting whose name does not
         * appear as a key in the mapping, its source setting is assumed to have the same name.
         */
        useAliases(masterManager, map) {
            this.manager = masterManager.mapTo(map);
            for (const aliasedSetting of this.aliasedSettings.values()) {
                aliasedSetting.useSource(this.manager.tryGetSetting(aliasedSetting.definition.name));
            }
        }
        /** @inheritdoc */
        tryGetSetting(name) {
            return this.aliasedSettings.get(name);
        }
        /** @inheritdoc */
        getSetting(name) {
            const setting = this.tryGetSetting(name);
            if (setting === undefined) {
                throw new Error(`AliasedUserSettingManager: Could not find setting with name ${name}`);
            }
            return setting;
        }
        /** @inheritdoc */
        whenSettingChanged(name) {
            const setting = this.aliasedSettings.get(name);
            if (!setting) {
                throw new Error(`AliasedUserSettingManager: Could not find setting with name ${name}`);
            }
            return new BasicConsumer((handler, paused) => {
                return setting.sub(handler, true, paused);
            }).whenChanged();
        }
        /** @inheritdoc */
        getAllSettings() {
            return Array.from(this.aliasedSettings.values());
        }
        /** @inheritdoc */
        mapTo(map) {
            return new MappedUserSettingManager(this, map);
        }
    }
    /**
     * A user setting with a value which is sourced from another setting. While the setting has no source, its value is
     * fixed to its default value and cannot be changed.
     */
    class AliasedUserSetting extends AbstractSubscribable {
        // eslint-disable-next-line jsdoc/require-returns
        /** This setting's current value. */
        get value() {
            var _a, _b;
            return (_b = (_a = this.setting) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : this.definition.defaultValue;
        }
        // eslint-disable-next-line jsdoc/require-jsdoc
        set value(v) {
            this.setting && (this.setting.value = v);
        }
        /**
         * Constructor.
         * @param definition This setting's definition.
         */
        constructor(definition) {
            super();
            this.definition = definition;
            this.isMutableSubscribable = true;
        }
        /**
         * Sets this setting's source.
         * @param setting The user setting to use as the new source, or `undefined` to leave this setting without a source.
         */
        useSource(setting) {
            var _a;
            const oldValue = this.value;
            (_a = this.settingSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.setting = setting;
            if (setting !== undefined) {
                this.settingSub = setting.sub(() => { this.notify(); });
            }
            else {
                this.settingSub = undefined;
            }
            if (oldValue !== this.value) {
                this.notify();
            }
        }
        /** @inheritdoc */
        get() {
            return this.value;
        }
        /**
         * Sets the value of this setting.
         * @param value The new value.
         */
        set(value) {
            this.value = value;
        }
        /** @inheritdoc */
        resetToDefault() {
            this.set(this.definition.defaultValue);
        }
    }

    /**
     * A manager for user settings that are saved and persistent across flight sessions. The manager facilitates saving
     * and loading setting values to and from multiple keyed save slots and also supports auto-saving. Uses Data Store to
     * store saved setting values.
     */
    class UserSettingSaveManager {
        /**
         * Constructor.
         * @param settings This manager's managed settings.
         * @param bus The event bus.
         */
        constructor(settings, bus) {
            this.autoSaveKeys = new Set();
            this.isAlive = true;
            const subscriber = bus.getSubscriber();
            this.entries = Array.from(settings, setting => {
                const autoSaveDataStoreKeys = [];
                return {
                    setting,
                    subscription: subscriber.on(setting.definition.name).whenChanged().handle(this.onSettingChanged.bind(this, autoSaveDataStoreKeys), true),
                    autoSaveDataStoreKeys
                };
            });
        }
        /**
         * A callback which is called when a setting's value changes.
         * @param autoSaveDataStoreKeys The data store keys to which the setting's value should be automatically saved.
         * @param value The new value of the setting.
         */
        onSettingChanged(autoSaveDataStoreKeys, value) {
            const len = autoSaveDataStoreKeys.length;
            for (let i = 0; i < len; i++) {
                exports.DataStore.set(autoSaveDataStoreKeys[i], value);
            }
        }
        /**
         * Loads the saved values of this manager's settings.
         * @param key The key from which to load the values.
         * @throws Error if this manager has been destroyed.
         */
        load(key) {
            if (!this.isAlive) {
                throw new Error('UserSettingSaveManager: cannot load using a destroyed manager.');
            }
            for (let i = 0; i < this.entries.length; i++) {
                const entry = this.entries[i];
                const dataStoreKey = UserSettingSaveManager.getDataStoreKey(entry.setting, key);
                const storedValue = exports.DataStore.get(dataStoreKey);
                if (storedValue !== undefined) {
                    entry.setting.value = storedValue;
                }
            }
        }
        /**
         * Saves the current values of this manager's settings.
         * @param key The key to which to save the values.
         * @throws Error if this manager has been destroyed.
         */
        save(key) {
            if (!this.isAlive) {
                throw new Error('UserSettingSaveManager: cannot save using a destroyed manager.');
            }
            for (let i = 0; i < this.entries.length; i++) {
                const entry = this.entries[i];
                const dataStoreKey = UserSettingSaveManager.getDataStoreKey(entry.setting, key);
                exports.DataStore.set(dataStoreKey, entry.setting.value);
            }
        }
        /**
         * Starts automatically saving this manager's settings when their values change.
         * @param key The key to which to save the values.
         * @throws Error if this manager has been destroyed.
         */
        startAutoSave(key) {
            if (!this.isAlive) {
                throw new Error('UserSettingSaveManager: cannot start autosave using a destroyed manager.');
            }
            if (this.autoSaveKeys.has(key)) {
                return;
            }
            for (let i = 0; i < this.entries.length; i++) {
                const entry = this.entries[i];
                entry.autoSaveDataStoreKeys.push(UserSettingSaveManager.getDataStoreKey(entry.setting, key));
                if (entry.autoSaveDataStoreKeys.length === 1) {
                    entry.subscription.resume();
                }
            }
        }
        /**
         * Stops automatically saving this manager's settings when their values change.
         * @param key The key to which to stop saving the values.
         * @throws Error if this manager has been destroyed.
         */
        stopAutoSave(key) {
            if (!this.isAlive) {
                throw new Error('UserSettingSaveManager: cannot stop autosave using a destroyed manager.');
            }
            if (!this.autoSaveKeys.has(key)) {
                return;
            }
            for (let i = 0; i < this.entries.length; i++) {
                const entry = this.entries[i];
                entry.autoSaveDataStoreKeys.splice(entry.autoSaveDataStoreKeys.indexOf(UserSettingSaveManager.getDataStoreKey(entry.setting, key)), 1);
                if (entry.autoSaveDataStoreKeys.length === 0) {
                    entry.subscription.pause();
                }
            }
        }
        /**
         * Destroys this manager. Once this manager is destroyed, all active autosaves will be stopped, and attempting to
         * save, load, or start another autosave from this manager will cause an error to be thrown.
         */
        destroy() {
            const len = this.entries.length;
            for (let i = 0; i < len; i++) {
                this.entries[i].subscription.destroy();
            }
            this.entries.length = 0;
            this.isAlive = false;
        }
        /**
         * Gets a data store key for a specific setting and save key.
         * @param setting A user setting.
         * @param saveKey The save key.
         * @returns the data store key for the setting and save key.
         */
        static getDataStoreKey(setting, saveKey) {
            return `${UserSettingSaveManager.DATASTORE_PREFIX}.${saveKey}.${setting.definition.name}`;
        }
    }
    UserSettingSaveManager.DATASTORE_PREFIX = 'persistent-setting';

    /**
     * The state of an avionics system.
     */
    exports.AvionicsSystemState = void 0;
    (function (AvionicsSystemState) {
        AvionicsSystemState["Off"] = "Off";
        AvionicsSystemState["Initializing"] = "Initializing";
        AvionicsSystemState["On"] = "On";
        AvionicsSystemState["Failed"] = "Failed";
    })(exports.AvionicsSystemState || (exports.AvionicsSystemState = {}));

    /**
     * A basic avionics system with a fixed initialization time and logic.
     */
    class BasicAvionicsSystem {
        /** @inheritdoc */
        get state() {
            return this._state;
        }
        /**
         * Creates an instance of a BasicAvionicsSystem.
         * @param index The index of the system.
         * @param bus The instance of the event bus for the system to use.
         * @param stateEvent The key of the state update event to send on state update.
         */
        constructor(index, bus, stateEvent) {
            this.index = index;
            this.bus = bus;
            this.stateEvent = stateEvent;
            /** The time it takes in milliseconds for the system to initialize. */
            this.initializationTime = 0;
            /** A timeout after which initialization will be complete. */
            this.initializationTimer = new DebounceTimer();
            this.publisher = this.bus.getPublisher();
            /** Whether power data consumed by this system is valid. */
            this.isPowerValid = false;
            this.bus.pub(this.stateEvent, { previous: undefined, current: undefined });
            // When not starting cold and dark (on runway or in air), electrical power simvars are not properly initialized
            // during loading, so we will ignore all power data until the game enters briefing state.
            const gameStateSub = GameStateProvider.get().sub(state => {
                if (state === GameState.briefing || state === GameState.ingame) {
                    gameStateSub.destroy();
                    this.isPowerValid = true;
                    this.onPowerValid();
                }
            }, false, true);
            gameStateSub.resume(true);
        }
        /**
         * Responds to when power data becomes valid.
         */
        onPowerValid() {
            var _a;
            (_a = this.electricalPowerSub) === null || _a === void 0 ? void 0 : _a.resume(true);
        }
        /**
         * Connects this system's power state to an {@link ElectricalEvents} topic, electricity logic element, or
         * {@link Subscribable}.
         * @param source The source to which to connect this system's power state.
         */
        connectToPower(source) {
            var _a;
            (_a = this.electricalPowerSub) === null || _a === void 0 ? void 0 : _a.destroy();
            this.electricalPowerSub = undefined;
            this.electricalPowerLogic = undefined;
            if (typeof source === 'string') {
                this.electricalPowerSub = this.bus.getSubscriber()
                    .on(source)
                    .whenChanged()
                    .handle(this.onPowerChanged.bind(this), !this.isPowerValid);
            }
            else if (SubscribableUtils.isSubscribable(source)) {
                this.electricalPowerSub = source.sub(this.onPowerChanged.bind(this), true, !this.isPowerValid);
            }
            else {
                this.electricalPowerLogic = source;
                this.updatePowerFromLogic();
            }
        }
        /**
         * Sets the state of the avionics system and publishes the change.
         * @param state The new state to change to.
         */
        setState(state) {
            if (this._state !== state) {
                const previous = this._state;
                this._state = state;
                this.onStateChanged(previous, state);
                this.bus.pub(this.stateEvent, { previous, current: state });
            }
        }
        /**
         * Responds to changes in this system's state.
         * @param previousState The previous state.
         * @param currentState The current state.
         */
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        onStateChanged(previousState, currentState) {
            // noop
        }
        /**
         * A callback called when the connected power state of the avionics system changes.
         * @param isPowered Whether or not the system is powered.
         */
        onPowerChanged(isPowered) {
            const wasPowered = this.isPowered;
            this.isPowered = isPowered;
            if (wasPowered === undefined) {
                this.initializationTimer.clear();
                if (isPowered) {
                    this.setState(exports.AvionicsSystemState.On);
                }
                else {
                    this.setState(exports.AvionicsSystemState.Off);
                }
            }
            else {
                if (isPowered) {
                    this.setState(exports.AvionicsSystemState.Initializing);
                    this.initializationTimer.schedule(() => this.setState(exports.AvionicsSystemState.On), this.initializationTime);
                }
                else {
                    this.initializationTimer.clear();
                    this.setState(exports.AvionicsSystemState.Off);
                }
            }
        }
        /** @inheritdoc */
        onUpdate() {
            this.updatePowerFromLogic();
        }
        /**
         * Updates this system's power state from an electricity logic element.
         */
        updatePowerFromLogic() {
            if (!this.isPowerValid || this.electricalPowerLogic === undefined) {
                return;
            }
            const isPowered = this.electricalPowerLogic.getValue() !== 0;
            if (isPowered !== this.isPowered) {
                this.onPowerChanged(isPowered);
            }
        }
    }

    /** A parser for FmsConfig elements. */
    class ConfigParser {
        /**
         * Returns a value computed by an executor, with a fallback value if the executor throws
         *
         * @param executor the executor which returns the value
         * @param defaultValue a fallback value in case the executor throws
         *
         * @returns T
         */
        static optional(executor, defaultValue) {
            let value = defaultValue;
            try {
                value = executor();
            }
            catch (e) {
                // noop
            }
            return value;
        }
        /**
         * Gets the first occurrence of a given child element.
         * @param parentElement The element to search on.
         * @param tagName The tag name to find.
         * @returns An element.
         * @throws If no element is found.
         */
        static getChildElement(parentElement, tagName) {
            const element = parentElement.querySelector(`:scope > ${tagName}`);
            if (!element) {
                throw new Error(`${ConfigParser.configName}: Element <${tagName}> must exist as a child of <${parentElement.tagName}>`);
            }
            return element;
        }
        /**
         * Gets all occurrences of a given child element type.
         * @param parentElement The element to search on.
         * @param tagName The tag name to find.
         * @returns All child elements that match.
         */
        static getChildElements(parentElement, tagName) {
            const elements = parentElement.querySelectorAll(`:scope > ${tagName}`);
            return Array.from(elements);
        }
        /**
         * Gets a LookupTable child of an element
         * @param parentElement The element to search on.
         * @param tagName The tag name to find.
         * @returns LerpLookupTable
         * @throws If a lookup table cannot be parsed
         */
        static getChildLerpLookupTable(parentElement, tagName) {
            const element = parentElement.querySelector(`:scope > ${tagName} > LookupTable`);
            if (!element) {
                throw new Error('Invalid LookupTableConfig definition: cannot find tag');
            }
            if (element.tagName !== 'LookupTable') {
                throw new Error(`Invalid LookupTableConfig definition: expected tag name 'LookupTable' but was '${element.tagName}'`);
            }
            const dimensions = element.getAttribute('dimensions');
            if (dimensions === null) {
                throw new Error('Invalid LookupTableConfig definition: undefined \'dimensions\' attribute');
            }
            const parsedDimensions = Number(dimensions);
            if (isNaN(parsedDimensions) || Math.trunc(parsedDimensions) !== parsedDimensions || parsedDimensions <= 0) {
                throw new Error(`Invalid LookupTableConfig definition: expected 'dimensions' to be a positive integer but was '${dimensions}'`);
            }
            const value = element.textContent;
            if (value === null) {
                throw new Error('Invalid LookupTableConfig definition: undefined value');
            }
            let parsedValue = undefined;
            try {
                parsedValue = JSON.parse(value);
            }
            catch (_a) {
                // continue
            }
            if (parsedValue instanceof Array) {
                for (const breakpoint of parsedValue) {
                    if (!(breakpoint instanceof Array && breakpoint.length === parsedDimensions + 1 && breakpoint.every(el => typeof el === 'number'))) {
                        throw new Error('Invalid LookupTableConfig definition: malformed lookup table array');
                    }
                }
            }
            else {
                throw new Error('Invalid LookupTableConfig definition: value was not an array');
            }
            const table = new LerpLookupTable(parsedDimensions);
            for (const breakpoint of parsedValue) {
                table.insertBreakpoint(breakpoint);
            }
            return table;
        }
        /**
         * Gets the text content of an element.
         * @param element The element to search on.
         * @returns The element's text content.
         * @throws If no text content is present.
         */
        static getTextContent(element) {
            const elementText = element.textContent;
            if (elementText === null || elementText.length < 1) {
                throw new Error(`${ConfigParser.configName}: Element <'${element.tagName}'> must have a text content with nonzero length`);
            }
            return elementText;
        }
        /**
         * Gets an attribute value of an element.
         * @param element The element to search on.
         * @param attributeName The attribute to find on the element.
         * @returns The attribute's raw (string) value.
         * @throws If the attribute is not present.
         */
        static getAttribute(element, attributeName) {
            const attribute = element.getAttribute(attributeName);
            if (attribute === null) {
                throw new Error(`${ConfigParser.configName}: Attribute '${attributeName}' must exist on <${element.tagName}>`);
            }
            return attribute;
        }
        /**
         * Gets a string attribute value of an element.
         * @param element The element to search on.
         * @param attributeName The attribute to find on the element.
         * @returns The attribute's value.
         * @throws If the attribute is not present.
         */
        static getStringAttrValue(element, attributeName) {
            return ConfigParser.getAttribute(element, attributeName);
        }
        /**
         * Gets an integer attribute value of an element.
         * @param element The element to search on.
         * @param attributeName The attribute to find on the element.
         * @param allowZero Whether zero should be allowed as a value.
         * @param allowNegative Whether negative numbers should be allowed as a value.
         * @returns The attribute's value.
         * @throws If the attribute is not present.
         */
        static getIntegerAttrValue(element, attributeName, allowZero = false, allowNegative = false) {
            const attribute = ConfigParser.getAttribute(element, attributeName);
            const num = parseInt(attribute);
            if (!Number.isInteger(num)) {
                throw new Error(`${ConfigParser.configName}: Attribute '${attributeName}' on <${element.tagName}> must be an integer`);
            }
            if (num < 0 && !allowNegative) {
                throw new Error(`${ConfigParser.configName}: Attribute '${attributeName}' on <${element.tagName}> must be a positive integer`);
            }
            if (num === 0 && !allowZero) {
                throw new Error(`${ConfigParser.configName}: Attribute '${attributeName}' on <${element.tagName}> must be a nonzero integer`);
            }
            return num;
        }
    }
    ConfigParser.configName = 'ConfigParser';
    /** A base for FMS config builder classes. */
    class ConfigBuilder {
        /**
         * Constructor
         * @param baseConfigElement XML config element to parse
         * @param baseInstrument the base instrument class
         * @param errorsMap Whether there were any errors encountered while parsing the config.
         */
        constructor(baseConfigElement, baseInstrument, errorsMap = new Map()) {
            this.baseConfigElement = baseConfigElement;
            this.baseInstrument = baseInstrument;
            this.errorsMap = errorsMap;
        }
        /**
         * Gets the config element.
         * @returns The config element.
         */
        get configElement() {
            return ConfigParser.getChildElement(this.baseConfigElement, this.CONFIG_TAG_NAME);
        }
        /**
         * Returns a config object.
         * @returns A parsed config, or its default value if parsing errors were encountered.
         */
        getConfig() {
            ConfigParser.configName = this.CONFIG_TAG_NAME;
            try {
                this.errorsMap.set(this.CONFIG_TAG_NAME, false);
                return this.parseConfig();
            }
            catch (e) {
                console.error(`ConfigParser: Failed to parse the <${ConfigParser.configName}> config from panel.xml. Using default values instead.`);
                console.error(e);
                this.errorsMap.set(this.CONFIG_TAG_NAME, true);
                return this.defaultConfig();
            }
        }
        /**
         * Returns a map of which configs encountered parsing errors and which did not.
         * @returns A map with config name keys and boolean values indicating whether a parsing error was encountered.
         */
        getErrors() {
            return this.errorsMap;
        }
    }

    /**
     * Contains data necessary for predicting flight plan legs
     */
    class FlightPlanPredictorStore {
        /**
         * Ctor
         *
         * @param bus           the event bus
         * @param flightPlanner a flight planner
         * @param planIndexSub  a subscribable regarding the index of the flight plan we want to predict for
         */
        constructor(bus, flightPlanner, planIndexSub) {
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.planIndexSub = planIndexSub;
            this.activeLegSubject = Subject.create(null);
            this.ppos = ConsumerSubject.create(null, new LatLongAlt());
            this.groundSpeed = ConsumerSubject.create(null, 150);
            this.altitude = ConsumerSubject.create(null, -1);
            /**
             * Total fuel quantity in gallons
             */
            this.fuelTotalQuantity = ConsumerSubject.create(null, 0);
            /**
             * Total fuel quantity in gallons per hour
             */
            this.fuelFlow = ConsumerSubject.create(null, 0);
            /**
             * Fuel weight in pounds per gallons
             */
            this.fuelWeight = ConsumerSubject.create(null, 0);
            this.lnavDtg = ConsumerSubject.create(null, 0);
            this.unixSimTime = ConsumerSubject.create(null, 0);
            const sub = this.bus.getSubscriber();
            this.ppos.setConsumer(sub.on('gps-position').atFrequency(1));
            this.groundSpeed.setConsumer(sub.on('ground_speed'));
            this.altitude.setConsumer(sub.on('pressure_alt'));
            this.fuelFlow.setConsumer(sub.on('fuel_flow_total'));
            this.fuelTotalQuantity.setConsumer(sub.on('fuel_total'));
            this.fuelWeight.setConsumer(sub.on('fuel_weight_per_gallon'));
            this.lnavDtg.setConsumer(sub.on('lnavdata_waypoint_distance'));
            this.unixSimTime.setConsumer(sub.on('simTime'));
            this.flightPlanner.onEvent('fplActiveLegChange').handle((data) => {
                if (data.planIndex === this.planIndexSub.get()) {
                    this.handleNewActiveLeg();
                }
            });
            this.flightPlanner.onEvent('fplCopied').handle((data) => {
                if (data.planIndex === this.planIndexSub.get()) {
                    this.handleNewActiveLeg();
                }
            });
        }
        /**
         * Handles the active leg changing
         */
        handleNewActiveLeg() {
            const plan = this.flightPlanner.getFlightPlan(this.planIndexSub.get());
            const activeLegIndex = plan.activeLateralLeg;
            const activeLeg = plan.tryGetLeg(activeLegIndex);
            this.activeLegSubject.set(activeLeg);
        }
    }

    /**
     * Utilities for {@link FlightPlanPredictor}
     */
    class FlightPlanPredictorUtils {
        /**
         * noop
         */
        constructor() {
            // noop
        }
        /**
         * Predicts time to fly a distance at a ground speed
         *
         * @param gs       the GPS ground speed in knots
         * @param distance the distance in nautical miles
         *
         * @returns the predicted time in seconds duration
         */
        static predictTime(gs, distance) {
            return UnitType.HOUR.convertTo(distance / gs, UnitType.SECOND);
        }
        /**
         * Predicts fuel usage to fly a distance at a ground speed with a fuel flow and weight
         *
         * @param gs         the GPS ground speed in knots
         * @param distance   the distance in nautical miles
         * @param fuelFlow   the total fuel flow in gallons per hour
         * @param fuelWeight the fuel weight in pounds per gallon
         *
         * @returns the predicted fuel usage in pounds
         */
        static predictFuelUsage(gs, distance, fuelFlow, fuelWeight) {
            const fuelVolumeUsed = UnitType.SECOND.convertTo(FlightPlanPredictorUtils.predictTime(gs, distance), UnitType.HOUR) * fuelFlow;
            return fuelVolumeUsed * fuelWeight;
        }
    }

    /**
     * Creates leg-by-leg predictions for a flight plan, both in the future by estimating performance and in the past by
     * recording predicted data and actual achieved performance.
     */
    class FlightPlanPredictor {
        /**
         * Ctor
         *
         * @param bus               the event bus
         * @param flightPlanner     a flight planner
         * @param planIndexSub      a subscribable regarding the index of the flight plan we want to predict for
         * @param activeLegIndexSub a subscribable regarding the index of the displayed active leg, specific to the avionics suite
         * @param config            configuration object
         */
        constructor(bus, flightPlanner, planIndexSub, activeLegIndexSub, config) {
            this.bus = bus;
            this.flightPlanner = flightPlanner;
            this.planIndexSub = planIndexSub;
            this.activeLegIndexSub = activeLegIndexSub;
            this.config = config;
            this.predictions = [];
            this.facilityPredictions = new Map();
            this.store = new FlightPlanPredictorStore(this.bus, this.flightPlanner, this.planIndexSub);
        }
        /**
         * Whether the flight plan exists and has an active lateral leg index >= 1
         *
         * @returns boolean
         */
        get planAndPredictionsValid() {
            if (this.flightPlanner.hasFlightPlan(this.planIndexSub.get()) && this.isAllLegsCalculated()) {
                return this.activeLegIndex >= 1;
            }
            return false;
        }
        /**
         * Obtains the flight plan to predict
         *
         * @returns a flight plan
         */
        get plan() {
            return this.flightPlanner.getFlightPlan(this.planIndexSub.get());
        }
        /**
         * Returns the active leg index to be used
         *
         * @returns the index
         */
        get activeLegIndex() {
            return this.activeLegIndexSub.get();
        }
        /**
         * Checks if all legs in the plan are calculated
         * @returns true if all legs are calculated, false otherwise
         */
        isAllLegsCalculated() {
            // check all legs are calculated from generator
            for (const leg of this.plan.legs(false, this.activeLegIndex)) {
                if (!leg.calculated) {
                    return false;
                }
            }
            return true;
        }
        /**
         * Updates the predictor
         */
        update() {
            if (!this.planAndPredictionsValid) {
                this.clearOutValues();
                return;
            }
            const activeLegIndex = this.activeLegIndex;
            const prevLegIndex = activeLegIndex - 1;
            // Return if no active leg
            if (!this.plan.tryGetLeg(prevLegIndex)) {
                return;
            }
            // Update all legs
            let accumulatedDistance = this.store.lnavDtg.get();
            let maxIndex = -1;
            let lastNonDiscontinuityLeg = undefined;
            for (const [i, leg, previousLeg] of this.predictableLegs()) {
                maxIndex = i;
                if ((previousLeg === null || previousLeg === void 0 ? void 0 : previousLeg.leg.type) === exports.LegType.Discontinuity || (previousLeg === null || previousLeg === void 0 ? void 0 : previousLeg.leg.type) === exports.LegType.ThruDiscontinuity) {
                    if (lastNonDiscontinuityLeg !== undefined && lastNonDiscontinuityLeg.calculated && leg.calculated) {
                        const termLat = lastNonDiscontinuityLeg.calculated.endLat;
                        const termLon = lastNonDiscontinuityLeg.calculated.endLon;
                        const startLat = leg.calculated.endLat;
                        const startLon = leg.calculated.endLon;
                        if (termLat && termLon && startLat && startLon) {
                            const gaRadDistance = new GeoPoint(termLat, termLon).distance(new GeoPoint(startLat, startLon));
                            accumulatedDistance += UnitType.NMILE.convertFrom(gaRadDistance, UnitType.GA_RADIAN);
                        }
                    }
                }
                lastNonDiscontinuityLeg = leg;
                const isPassedLeg = i < activeLegIndex;
                const isActiveLeg = i === activeLegIndex;
                const isUpcomingLeg = i > activeLegIndex;
                const oldPredictions = this.predictionsForLegIndex(i);
                if (oldPredictions) {
                    if (isPassedLeg) {
                        if (oldPredictions.kind === 'activeOrUpcoming') {
                            this.stampPassedLegValues(oldPredictions);
                        }
                        this.updatePassedLeg(oldPredictions, leg);
                    }
                    else if (isActiveLeg) {
                        this.updateActiveLeg(oldPredictions);
                    }
                    else {
                        this.updateUpcomingLeg(oldPredictions, leg, accumulatedDistance);
                    }
                    if (isActiveLeg || isUpcomingLeg) {
                        accumulatedDistance += oldPredictions.distance - accumulatedDistance;
                    }
                }
                else {
                    const newPredictions = {};
                    if (isPassedLeg) {
                        this.updatePassedLeg(newPredictions, leg);
                    }
                    else if (isActiveLeg) {
                        this.updateActiveLeg(newPredictions);
                    }
                    else {
                        this.updateUpcomingLeg(newPredictions, leg, accumulatedDistance);
                    }
                    if (isActiveLeg || isUpcomingLeg) {
                        accumulatedDistance += newPredictions.distance - accumulatedDistance;
                    }
                    this.predictions[i] = newPredictions;
                }
            }
            if (maxIndex > 0) {
                for (let i = maxIndex; i < this.predictions.length - 1; i++) {
                    this.predictions.pop();
                }
            }
            this.clearOutDirtyValues();
        }
        /**
         * Clears out values from predictions
         *
         * @private
         */
        clearOutValues() {
            this.predictions.length = 0;
        }
        /**
         * Clears out entries that have become discontinuities
         */
        clearOutDirtyValues() {
            for (let i = 0; i < this.plan.length; i++) {
                const leg = this.plan.getLeg(i);
                if (leg.leg.type === exports.LegType.Discontinuity || leg.leg.type === exports.LegType.ThruDiscontinuity) {
                    this.predictions.splice(i, 1, undefined);
                }
            }
        }
        /**
         * Finds the index of the destination leg, in other words, the last non-missed-approach leg.
         *
         * @returns the index, or -1 if not applicable
         */
        findDestinationLegIndex() {
            let lastLegIndex = this.plan.length - 1;
            for (const leg of this.plan.legs(true)) {
                if (!BitFlags.isAll(leg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                    break;
                }
                lastLegIndex--;
            }
            return lastLegIndex < 1 ? -1 : lastLegIndex;
        }
        /**
         * Iterator for existing predictions
         *
         * @param startAtIndex the index to start at
         *
         * @returns a generator
         *
         * @yields predictions
         */
        *iteratePredictions(startAtIndex = 0) {
            for (let i = startAtIndex; i < this.predictions.length; i++) {
                yield this.predictions[i];
            }
        }
        /**
         * Iterator for existing predictions in reverse
         *
         * @param startAtIndex the index to start at
         *
         * @returns a generator
         *
         * @yields predictions
         */
        *iteratePredictionsReverse(startAtIndex = 0) {
            for (let i = startAtIndex; i >= 0; i--) {
                yield this.predictions[i];
            }
        }
        /**
         * Returns predictions for the destination airport.
         *
         * If the dest leg (defined as the last leg that is not part of the missed approach) is not a runway,
         * then the direct distance between the termination of that leg and the provided airport facility is added to
         * the result. Otherwise, the prediction to that leg is used.
         *
         * @param destinationFacility the airport facility to use in case a direct distance needs to be calculated
         *
         * @returns predictions for the destination airport, or null if they cannot be computed
         */
        getDestinationPrediction(destinationFacility) {
            var _a, _b, _c, _d, _e;
            const destLegIndex = this.findDestinationLegIndex();
            const leg = this.plan.tryGetLeg(destLegIndex);
            if (!leg) {
                return this.getPposToFacilityPredictions(destinationFacility);
            }
            const destLegHasValidFixIcao = leg.leg.fixIcao && leg.leg.fixIcao !== ICAO.emptyIcao;
            const isDestLegRunway = destLegHasValidFixIcao ? ICAO.getFacilityType(leg.leg.fixIcao) === exports.FacilityType.RWY : false;
            if (!isDestLegRunway && ((_a = leg.calculated) === null || _a === void 0 ? void 0 : _a.endLat) && leg.calculated.endLon) {
                const legTerm = new GeoPoint((_b = leg.calculated) === null || _b === void 0 ? void 0 : _b.endLat, (_c = leg.calculated) === null || _c === void 0 ? void 0 : _c.endLon);
                const airport = new GeoPoint(destinationFacility.lat, destinationFacility.lon);
                const additionalDirectDistance = UnitType.GA_RADIAN.convertTo(legTerm.distance(airport), UnitType.NMILE);
                const predictionsToDestLeg = this.predictionsForLegIndex(destLegIndex);
                if (predictionsToDestLeg) {
                    const directPredictions = {
                        kind: 'activeOrUpcoming',
                        ident: '',
                        distance: additionalDirectDistance,
                        estimatedTimeOfArrival: 0,
                        estimatedTimeEnroute: 0,
                        fob: 0,
                    };
                    this.predictForDistance(directPredictions, additionalDirectDistance);
                    directPredictions.estimatedTimeEnroute = FlightPlanPredictorUtils.predictTime(this.currentGs(), additionalDirectDistance);
                    const fuelConsumedOnDirect = Math.max(0, this.currentFuelWeight() - ((_d = directPredictions.fob) !== null && _d !== void 0 ? _d : 0));
                    return {
                        kind: 'activeOrUpcoming',
                        ident: ICAO.getIdent(destinationFacility.icao),
                        estimatedTimeOfArrival: predictionsToDestLeg.estimatedTimeOfArrival + directPredictions.estimatedTimeEnroute,
                        estimatedTimeEnroute: predictionsToDestLeg.estimatedTimeEnroute + directPredictions.estimatedTimeEnroute,
                        distance: predictionsToDestLeg.distance + additionalDirectDistance,
                        fob: Math.max(0, ((_e = predictionsToDestLeg.fob) !== null && _e !== void 0 ? _e : this.currentFuelWeight()) - fuelConsumedOnDirect),
                    };
                }
                else {
                    return null;
                }
            }
            else {
                const predictionsForLegIndex = this.predictionsForLegIndex(destLegIndex);
                if (predictionsForLegIndex) {
                    return Object.assign(Object.assign({}, predictionsForLegIndex), { ident: ICAO.getIdent(destinationFacility.icao) });
                }
                else {
                    return null;
                }
            }
        }
        /**
         * Returns predictions for an arbitrary facility.
         *
         * The distance used for predictions is the great circle distance between PPOS and the given facility.
         *
         * @param facility the facility to use
         *
         * @returns predictions for the facility
         */
        getPposToFacilityPredictions(facility) {
            const ppos = this.store.ppos.get();
            const distance = new GeoPoint(ppos.lat, ppos.long).distance({ lat: facility.lat, lon: facility.lon });
            const distanceNM = UnitType.NMILE.convertFrom(distance, UnitType.GA_RADIAN);
            const existingPredictions = this.facilityPredictions.get(facility.icao);
            let predictions;
            if (existingPredictions) {
                predictions = existingPredictions;
            }
            else {
                predictions = {
                    kind: 'activeOrUpcoming',
                    ident: ICAO.getIdent(facility.icao),
                    distance: distanceNM,
                    estimatedTimeOfArrival: 0,
                    estimatedTimeEnroute: 0,
                    fob: 0,
                };
            }
            this.predictForDistance(predictions, distanceNM);
            return predictions;
        }
        /**
         * Returns active or upcoming predictions for a given leg index
         *
         * @param index the leg index
         *
         * @returns the predictions object, or null if they cannot be computed
         */
        predictionsForLegIndex(index) {
            return this.predictions[index];
        }
        /**
         * Returns active or upcoming predictions for a given leg definition
         *
         * @param leg the leg
         *
         * @returns the predictions object, or null if they cannot be computed
         */
        predictionsForLeg(leg) {
            const index = this.plan.getLegIndexFromLeg(leg);
            if (index === -1) {
                return null;
            }
            return this.predictionsForLegIndex(index);
        }
        /**
         * Applies active or upcoming predictions for a given distance, outputting the result in the {@link out} argument
         *
         * @param distance the distance
         * @param out      the object in which to output the predictions
         */
        applyPredictionsForDistance(distance, out) {
            this.predictForDistance(out, distance);
        }
        /**
         * Whether the leg at an index is predicted
         *
         * @param legIndex the target leg index
         *
         * @returns boolean
         */
        isLegIndexPredicted(legIndex) {
            return !!this.predictions[legIndex];
        }
        /**
         * Whether the leg is predicted
         *
         * @param leg the target leg
         *
         * @returns boolean
         */
        isLegPredicted(leg) {
            const index = this.plan.getLegIndexFromLeg(leg);
            return !!this.predictions[index];
        }
        /**
         * Returns the previous index for which a prediction exists. **Note: this will force an update to happen.**
         *
         * @param legIndex the leg index to start at
         *
         * @returns the index, or -1 if none is found
         */
        findPreviousPredictedLegIndex(legIndex) {
            this.update();
            for (let i = legIndex - 1; i >= 0; i--) {
                const isPredicted = this.isLegIndexPredicted(i);
                if (isPredicted) {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Returns the previous index for which a prediction exists. **Note: this will force an update to happen.**
         *
         * @param legIndex the leg index to start at
         *
         * @returns the index, or -1 if none is found
         */
        findNextPredictedLegIndex(legIndex) {
            this.update();
            for (let i = legIndex + 1; i < this.predictions.length; i++) {
                const isPredicted = this.isLegIndexPredicted(i);
                if (isPredicted) {
                    return i;
                }
            }
            return -1;
        }
        /**
         * Applies a reducer function to the predictions of active and upcoming legs
         *
         * @param initialValue initial accumulator value
         * @param reducer      reducer function
         * @param upTo         index to reduce to
         *
         * @returns reduced value
         */
        reducePredictions(initialValue, reducer, upTo = -1) {
            const limit = upTo === -1 ? this.predictions.length : upTo;
            let accumulator = initialValue;
            for (const [i] of this.predictableLegs(true)) {
                if (i > limit) {
                    break;
                }
                const predictions = this.predictionsForLegIndex(i);
                accumulator = reducer(accumulator, predictions);
            }
            return accumulator;
        }
        /**
         * Generator of all predictable legs in the plan
         *
         * The yielded tuple contains the following:
         * - 0: leg index in flight plan
         * - 1: leg definition object
         * - 2: previous leg definition object, including a previous discontinuity
         *
         * @param onlyAfterActive whether to start at the active leg
         *
         * @returns generator that skips appropriate legs
         *
         * @yields legs including and after the active leg that are not discontinuities (and not in missed approach, if config asks so)
         */
        *predictableLegs(onlyAfterActive = false) {
            let prevLeg = undefined;
            for (let i = onlyAfterActive ? this.activeLegIndex + 1 : 0; i < this.plan.length; i++) {
                const leg = this.plan.getLeg(i);
                // Skip discontinuities
                if (leg.leg.type === exports.LegType.Discontinuity || leg.leg.type === exports.LegType.ThruDiscontinuity) {
                    prevLeg = leg;
                    continue;
                }
                // Skip Direct To IF legs
                if (leg.leg.type === exports.LegType.IF && BitFlags.isAll(leg.flags, exports.LegDefinitionFlags.DirectTo)) {
                    prevLeg = leg;
                    continue;
                }
                // Stop at missed approach if configured to do so
                if (!this.config.predictMissedApproachLegs && BitFlags.isAll(leg.flags, exports.LegDefinitionFlags.MissedApproach)) {
                    break;
                }
                yield [i, leg, prevLeg];
                prevLeg = leg;
            }
        }
        /**
         * Stamps the actual values from the last estimated values
         *
         * @param targetObject the object to stamp the actual values on
         *
         * @private
         */
        stampPassedLegValues(targetObject) {
            targetObject.actualFob = targetObject.fob;
            targetObject.actualTimeEnroute = targetObject.estimatedTimeEnroute;
            targetObject.actualTimeOfArrival = targetObject.estimatedTimeOfArrival;
            targetObject.actualAltitude = this.store.altitude.get();
        }
        /**
         * Creates predictions for a passed leg
         *
         * @param targetObject the object to apply the predictions to
         * @param leg          the leg
         *
         * @throws if calculated is undefined
         */
        updatePassedLeg(targetObject, leg) {
            var _a;
            if (!leg.calculated || !leg.calculated.endLat || !leg.calculated.endLon) {
                return;
            }
            const term = new GeoPoint(leg.calculated.endLat, leg.calculated.endLon);
            const ppos = this.store.ppos.get();
            const distance = term.distance(new GeoPoint(ppos.lat, ppos.long));
            targetObject.kind = 'passed';
            targetObject.ident = (_a = leg.name) !== null && _a !== void 0 ? _a : 'n/a';
            targetObject.distance = UnitType.GA_RADIAN.convertTo(distance, UnitType.NMILE);
        }
        /**
         * Computes predictions for the active leg
         *
         * @param targetObject the object to apply the predictions to
         *
         * @throws if no active leg in flight plan
         */
        updateActiveLeg(targetObject) {
            var _a;
            const distance = this.store.lnavDtg.get();
            const leg = this.plan.tryGetLeg(this.activeLegIndex);
            if (!leg) {
                return;
            }
            targetObject.kind = 'activeOrUpcoming';
            targetObject.ident = (_a = leg.name) !== null && _a !== void 0 ? _a : 'n/a';
            targetObject.distance = distance;
            this.predictForDistance(targetObject, distance);
        }
        /**
         * Creates predictions for an upcoming leg
         *
         * @param targetObject        the object to apply the predictions to
         * @param leg                 the leg
         * @param accumulatedDistance accumulated distance in previous predictions before this leg
         */
        updateUpcomingLeg(targetObject, leg, accumulatedDistance) {
            var _a, _b;
            if (!leg.calculated) {
                return;
            }
            const ownDistance = UnitType.METER.convertTo((_a = leg.calculated) === null || _a === void 0 ? void 0 : _a.distanceWithTransitions, UnitType.NMILE);
            const distance = accumulatedDistance + ownDistance; // We do not use LegCalculations::cumulativeDistanceWithTransitions here, because
            // that does not account for PPOs
            targetObject.kind = 'activeOrUpcoming';
            targetObject.ident = (_b = leg.name) !== null && _b !== void 0 ? _b : 'n/a';
            targetObject.distance = distance;
            this.predictForDistance(targetObject, distance);
        }
        /**
         * Predicts performance over a distance
         *
         * @param targetObject        the object to apply the predictions to
         * @param distance            the distance flown
         */
        predictForDistance(targetObject, distance) {
            const estimatedTimeEnroute = FlightPlanPredictorUtils.predictTime(this.currentGs(), distance);
            const timeToDistance = FlightPlanPredictorUtils.predictTime(this.currentGs(), distance);
            const unixSeconds = UnitType.MILLISECOND.convertTo(this.store.unixSimTime.get(), UnitType.SECOND);
            const utcSeconds = unixSeconds % (3600 * 24);
            const estimatedTimeOfArrival = utcSeconds + timeToDistance;
            const fob = Math.max(0, this.currentFuelWeight() - FlightPlanPredictorUtils.predictFuelUsage(this.currentGs(), distance, this.store.fuelFlow.get(), this.store.fuelWeight.get()));
            targetObject.estimatedTimeEnroute = estimatedTimeEnroute;
            targetObject.estimatedTimeOfArrival = estimatedTimeOfArrival;
            targetObject.fob = fob;
        }
        /**
         * Obtains current GS with a minimum of 150
         *
         * @returns knots
         */
        currentGs() {
            return Math.max(this.config.minimumPredictionsGroundSpeed, this.store.groundSpeed.get());
        }
        /**
         * Obtains current fuel weight
         *
         * @returns pounds
         */
        currentFuelWeight() {
            return this.store.fuelTotalQuantity.get() * this.store.fuelWeight.get();
        }
    }

    /**
     * A manager for registering aural alerts. Alerts can be registered with an {@link AuralAlertSystem} through the
     * manager, which also handles registration requests from the alert system.
     */
    class AuralAlertRegistrationManager {
        /**
         * Creates a new instance of AuralAlertRegistrationManager.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.registrations = new Map();
            this.publisher = bus.getPublisher();
            const subscriber = bus.getSubscriber();
            this.requestSub = subscriber.on('aural_alert_request_all_registrations').handle(this.publishAllRegistrations.bind(this));
        }
        /**
         * Registers an aural alert.
         * @param definition The definition of the alert to register.
         */
        register(definition) {
            this.registrations.set(definition.uuid, definition);
            this.publishRegistration(definition);
        }
        /**
         * Publishes a registration event for an alert.
         * @param definition The definition of the alert to register.
         */
        publishRegistration(definition) {
            this.publisher.pub('aural_alert_register', definition, true, false);
        }
        /**
         * Publishes registration events for all currently registered alerts.
         */
        publishAllRegistrations() {
            for (const definition of this.registrations.values()) {
                this.publishRegistration(definition);
            }
        }
        /**
         * Destroys this manager.
         */
        destroy() {
            this.requestSub.destroy();
        }
    }

    /* eslint-disable @typescript-eslint/no-non-null-assertion */
    /**
     * A system which manages and plays aural alerts using a priority queue system.
     *
     * The system collects registered alerts, and manages how they are played. Each alert belongs to a queue. Only one
     * alert from each queue can play simultaneously. Alerts are queued to be played when they become activated or triggered.
     * If two alerts are queued at the same time, the one with higher priority is played first. Alerts cannot interrupt an
     * already playing alert, regardless of their relative priorities.
     */
    class AuralAlertSystem {
        /**
         * Creates a new AuralAlertSystem. The system is initially asleep after being created.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.soundServerSub = this.bus.getSubscriber();
            this.controlSub = this.bus.getSubscriber();
            this.soundServerPublisher = this.bus.getPublisher();
            this.publisher = this.bus.getPublisher();
            this.registeredAlerts = new Map();
            this.packetKeyToQueueMap = new Map();
            this.queues = new Map();
            this.playing = new Map();
            this.activeAliasToUuid = new Map();
            this.triggeredAliasToUuid = new Map();
            this.activeSuffixedIdToId = new Map();
            this.idToActiveSuffixedIds = new Map();
            this.triggeredSuffixedIdToId = new Map();
            this.idToTriggeredSuffixedIds = new Map();
            this.activeAlerts = new Map();
            this.triggeredAlerts = new Map();
            this.isSoundServerInit = false;
            this.isSoundServerAwake = false;
            this.isAwake = false;
            this.isActive = false;
            this.controlSub.on('aural_alert_register').handle(this.onAlertRegistered.bind(this));
            this.publisher.pub('aural_alert_request_all_registrations', undefined, true, false);
            this.soundServerSub.on('sound_server_is_awake').whenChanged().handle(this.onSoundServerWakeChanged.bind(this));
            this.soundServerSub.on('sound_server_packet_ended').handle(this.onPacketEnded.bind(this));
            this.controlSub.on('aural_alert_activate').handle(this.activateAlert.bind(this));
            this.controlSub.on('aural_alert_deactivate').handle(this.deactivateAlert.bind(this));
            this.controlSub.on('aural_alert_trigger').handle(this.triggerAlert.bind(this));
            this.controlSub.on('aural_alert_untrigger').handle(this.untriggerAlert.bind(this));
            this.controlSub.on('aural_alert_kill').handle(this.killAlert.bind(this));
            this.controlSub.on('aural_alert_deactivate_all').handle(this.deactivateAllAlerts.bind(this));
            this.controlSub.on('aural_alert_untrigger_all').handle(this.untriggerAllAlerts.bind(this));
            this.controlSub.on('aural_alert_kill_all').handle(this.killAllAlerts.bind(this));
            // Hold all pending alerts in their queues until the sound server is initialized. Then, start dequeuing alerts.
            Wait.awaitConsumer(this.soundServerSub.on('sound_server_initialized'), init => init, true).then(() => {
                this.isSoundServerInit = true;
                for (const queue of this.queues.values()) {
                    this.dequeueAlert(queue);
                }
            });
        }
        /**
         * Wakes this system. If the sound server is also awake, all active continuous or repeating alerts will be re-queued
         * to play. While both this system and the sound server are awake, activation of alerts will queue them to be played.
         * Activation of any alerts that were already active when the system woke up will not queue them to be played unless
         * the alert was deactivated in the interim.
         */
        wake() {
            if (this.isAwake) {
                return;
            }
            this.isAwake = true;
            this.updateActiveState();
        }
        /**
         * Puts this system to sleep. Clears all triggered and queued alerts and stops all currently playing alerts at the
         * earliest opportunity. While this system is asleep, activating alerts will not queue them to be played and
         * triggering alerts has no effect.
         */
        sleep() {
            if (!this.isAwake) {
                return;
            }
            this.isAwake = false;
            this.updateActiveState();
        }
        /**
         * Responds to when an alert is registered.
         * @param alert The definition of the registered alert.
         */
        onAlertRegistered(alert) {
            this.registeredAlerts.set(alert.uuid, alert);
            !this.queues.has(alert.queue) && this.createQueue(alert.queue);
        }
        /**
         * Creates an alert queue entry.
         * @param queueName The name of the queue to create.
         * @returns The new queue entry.
         */
        createQueue(queueName) {
            const entry = {
                name: queueName,
                tracks: new Map([['', this.createQueueTrack(queueName, '')]]),
                debounceTimer: new DebounceTimer()
            };
            this.queues.set(queueName, entry);
            return entry;
        }
        /**
         * Creates a queue track.
         * @param queueName The name of the queue.
         * @param trackName The name of the track.
         * @returns The new queue track.
         */
        createQueueTrack(queueName, trackName) {
            const packetKey = AuralAlertSystem.createPacketKey(queueName, trackName);
            this.packetKeyToQueueMap.set(packetKey, queueName);
            return {
                name: trackName,
                packetKey: packetKey,
                queue: new BinaryHeap(AuralAlertSystem.ALERT_COMPARATOR)
            };
        }
        /**
         * Checks if an alias is unique. An alias is considered unique if and only if it does not match any registered
         * alert IDs and it does not match any active or triggered aliases assigned to other parent IDs.
         * @param uuid The parent ID of the alias.
         * @param alias The alias to check.
         * @returns Whether the specified alias is unique.
         */
        isAliasUnique(uuid, alias) {
            if (this.registeredAlerts.has(alias)) {
                return false;
            }
            const existingActive = this.activeAliasToUuid.get(alias);
            if (existingActive !== undefined && existingActive !== uuid) {
                return false;
            }
            const existingTriggered = this.triggeredAliasToUuid.get(alias);
            if (existingTriggered !== undefined && existingTriggered !== uuid) {
                return false;
            }
            return true;
        }
        /**
         * Checks if a suffixed ID is unique. A suffixed ID is considered unique if and only if it does not match any
         * registered alert IDs or aliases and it does not match any active or triggered suffixed IDs assigned to other
         * parent IDs.
         * @param id The parent ID of the suffix.
         * @param suffixedId The suffixed ID to check.
         * @returns Whether the specified suffixed ID is unique.
         */
        isSuffixedIdUnique(id, suffixedId) {
            if (this.registeredAlerts.has(suffixedId) || this.activeAliasToUuid.has(suffixedId) || this.triggeredAliasToUuid.has(suffixedId)) {
                return false;
            }
            const existingActive = this.activeSuffixedIdToId.get(suffixedId);
            if (existingActive !== undefined && existingActive !== id) {
                return false;
            }
            const existingTriggered = this.triggeredSuffixedIdToId.get(suffixedId);
            if (existingTriggered !== undefined && existingTriggered !== id) {
                return false;
            }
            return true;
        }
        /**
         * Activates an alert.
         * @param activation The ID of the alert to activate, or data describing the alert to activate.
         */
        activateAlert(activation) {
            let uuid;
            let alias;
            let queuedId;
            let suffixedId;
            let activationObject;
            if (typeof activation === 'string') {
                uuid = activation;
                alias = undefined;
                queuedId = uuid;
                suffixedId = undefined;
                activationObject = undefined;
            }
            else {
                uuid = activation.uuid;
                alias = activation.alias;
                queuedId = alias !== null && alias !== void 0 ? alias : uuid;
                suffixedId = activation.suffix === undefined ? undefined : `${queuedId}::${activation.suffix}`;
                activationObject = activation;
            }
            const alertDef = this.registeredAlerts.get(uuid);
            // If the alert is not registered, then do nothing.
            if (!alertDef) {
                return;
            }
            if (alias !== undefined && !this.isAliasUnique(uuid, alias)) {
                return;
            }
            if (suffixedId !== undefined && !this.isSuffixedIdUnique(queuedId, suffixedId)) {
                return;
            }
            if (alias !== undefined) {
                this.activeAliasToUuid.set(alias, uuid);
            }
            this.activateSuffix(queuedId, suffixedId);
            // If the alert is already active, then do nothing.
            if (this.activeAlerts.has(queuedId)) {
                return;
            }
            const queuedAlert = this.createQueuedAlert(alertDef, activationObject);
            this.activeAlerts.set(queuedId, queuedAlert);
            if (this.isActive) {
                this.queueAlert(queuedAlert);
            }
        }
        /**
         * Activates an alert suffix.
         * @param id The ID of the suffix's parent alert.
         * @param suffixedId The suffixed ID to activate.
         */
        activateSuffix(id, suffixedId = id) {
            this.activeSuffixedIdToId.set(suffixedId, id);
            let suffixedIds = this.idToActiveSuffixedIds.get(id);
            if (!suffixedIds) {
                this.idToActiveSuffixedIds.set(id, suffixedIds = new Set());
            }
            suffixedIds.add(suffixedId);
        }
        /**
         * Triggers an alert.
         * @param activation The ID of the alert to trigger, or data describing the alert to trigger.
         */
        triggerAlert(activation) {
            var _a;
            if (!this.isActive) {
                return;
            }
            let uuid;
            let alias;
            let queuedId;
            let suffixedId;
            let activationObject;
            if (typeof activation === 'string') {
                uuid = activation;
                alias = undefined;
                queuedId = uuid;
                suffixedId = undefined;
                activationObject = undefined;
            }
            else {
                uuid = activation.uuid;
                alias = activation.alias;
                queuedId = alias !== null && alias !== void 0 ? alias : uuid;
                suffixedId = activation.suffix === undefined ? undefined : `${queuedId}::${activation.suffix}`;
                activationObject = activation;
            }
            const alertDef = this.registeredAlerts.get(uuid);
            // If the alert is not registered, then do nothing.
            if (!alertDef) {
                return;
            }
            if (alias !== undefined && !this.isAliasUnique(uuid, alias)) {
                return;
            }
            if (suffixedId !== undefined && !this.isSuffixedIdUnique(queuedId, suffixedId)) {
                return;
            }
            if (alias !== undefined) {
                this.triggeredAliasToUuid.set(alias, uuid);
            }
            this.triggerSuffix(queuedId, suffixedId);
            // If a triggered instance of this alert is already playing, then do nothing.
            const existing = this.triggeredAlerts.get(queuedId);
            if (existing && ((_a = this.playing.get(existing.definition.queue)) === null || _a === void 0 ? void 0 : _a.includes(existing))) {
                return;
            }
            const queuedAlert = this.createQueuedAlert(alertDef, activationObject);
            this.triggeredAlerts.set(queuedId, queuedAlert);
            this.queueAlert(queuedAlert);
        }
        /**
         * Triggers an alert suffix.
         * @param id The ID of the suffix's parent alert.
         * @param suffixedId The suffixed ID to trigger.
         */
        triggerSuffix(id, suffixedId = id) {
            this.triggeredSuffixedIdToId.set(suffixedId, id);
            let suffixedIds = this.idToTriggeredSuffixedIds.get(id);
            if (!suffixedIds) {
                this.idToTriggeredSuffixedIds.set(id, suffixedIds = new Set());
            }
            suffixedIds.add(suffixedId);
        }
        /**
         * Creates an alert to be queued.
         * @param definition The definition of the alert.
         * @param activation Data describing the alert to activate. If not defined, the alert will be activated according
         * to its definition.
         * @returns The queued alert.
         */
        createQueuedAlert(definition, activation) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j;
            const queueEntry = this.queues.get(definition.queue);
            const trackName = (_b = (_a = activation === null || activation === void 0 ? void 0 : activation.track) !== null && _a !== void 0 ? _a : definition.track) !== null && _b !== void 0 ? _b : '';
            let track = queueEntry.tracks.get(trackName);
            if (!track) {
                track = this.createQueueTrack(definition.queue, trackName);
                queueEntry.tracks.set(trackName, track);
            }
            return {
                definition,
                id: (_c = activation === null || activation === void 0 ? void 0 : activation.alias) !== null && _c !== void 0 ? _c : definition.uuid,
                track: trackName,
                repeat: (_d = activation === null || activation === void 0 ? void 0 : activation.repeat) !== null && _d !== void 0 ? _d : definition.repeat,
                packet: {
                    key: track.packetKey,
                    sequence: (_e = activation === null || activation === void 0 ? void 0 : activation.sequence) !== null && _e !== void 0 ? _e : definition.sequence,
                    continuous: (_f = activation === null || activation === void 0 ? void 0 : activation.continuous) !== null && _f !== void 0 ? _f : definition.continuous,
                    timeout: (_g = activation === null || activation === void 0 ? void 0 : activation.timeout) !== null && _g !== void 0 ? _g : definition.timeout
                },
                timestamp: Date.now(),
                queuedLifetime: (_j = (_h = activation === null || activation === void 0 ? void 0 : activation.queuedLifetime) !== null && _h !== void 0 ? _h : definition.queuedLifetime) !== null && _j !== void 0 ? _j : Infinity
            };
        }
        /**
         * Queues an alert to be played.
         * @param alert The alert to queue.
         */
        queueAlert(alert) {
            var _a;
            const queueName = alert.definition.queue;
            const queueEntry = (_a = this.queues.get(queueName)) !== null && _a !== void 0 ? _a : this.createQueue(queueName);
            queueEntry.tracks.get(alert.track).queue.insert(alert);
            if (this.isSoundServerInit) {
                const playingAlerts = this.playing.get(queueName);
                let isTrackPlaying = false;
                let playingPriority = undefined;
                if (playingAlerts) {
                    for (let i = 0; i < playingAlerts.length; i++) {
                        const playing = playingAlerts[i];
                        if (playing.track === alert.track) {
                            isTrackPlaying = true;
                        }
                        if (playingPriority === undefined || playing.definition.priority > playingPriority) {
                            playingPriority = playing.definition.priority;
                        }
                    }
                }
                // If the alert would be immediately dequeued (i.e. there is nothing playing on its track and there are no
                // currently playing alerts in its queue that are of higher priority), then wait one frame before we start
                // dequeuing so that alerts that are activated on the same frame are correctly prioritized.
                if (!isTrackPlaying && (playingPriority === undefined || alert.definition.priority >= playingPriority)) {
                    if (!queueEntry.debounceTimer.isPending()) {
                        queueEntry.debounceTimer.schedule(this.dequeueAlert.bind(this, queueEntry), 0);
                    }
                }
            }
        }
        /**
         * Dequeues the next activated or triggered alert from a queue and starts playing it. If this system is asleep, then
         * the queue will be cleared instead and no alert will be played.
         * @param entry The queue entry.
         */
        dequeueAlert(entry) {
            if (this.isActive) {
                const time = Date.now();
                let playingAlerts = this.playing.get(entry.name);
                // Go through each track of the queue and mark queued alerts to potentially dequeue and play.
                const nextAlerts = new BinaryHeap(AuralAlertSystem.ALERT_COMPARATOR);
                for (const track of entry.tracks.values()) {
                    // If an alert is still playing on this track, then we will not attempt to dequeue any alerts on this track.
                    if (playingAlerts && playingAlerts.find(query => query.track === track.name)) {
                        continue;
                    }
                    while (track.queue.size > 0) {
                        const next = track.queue.findMin();
                        // We need to compare the queued alert with the one in the active/triggered alerts map by reference instead
                        // of just comparing their IDs because the alert could have been deactivated and activated or triggered again
                        // while it was queued. The maps contain the queued alert object from the most recent activation/trigger, so
                        // that is the one we want to play.
                        if (time - next.timestamp <= next.queuedLifetime
                            && (this.activeAlerts.get(next.id) === next
                                || this.triggeredAlerts.get(next.id) === next)) {
                            // If the queued alert was not expired, replaced, deactivated, or untriggered, then mark it as a candidate
                            // to be dequeued and played. However, we will not remove it from the track queue yet because we don't know
                            // yet whether it will actually be dequeued.
                            nextAlerts.insert(next);
                            break;
                        }
                        else {
                            // If the queued alert was expired, replaced, deactivated, or untriggered, then remove it from the queue.
                            track.queue.removeMin();
                        }
                    }
                }
                if (nextAlerts.size === 0) {
                    return;
                }
                // Now we will evaluate every queued alert marked as a candidate for dequeuing in order of decreasing priority.
                // If there are no currently playing alerts that have a higher priority than the candidate with the highest
                // priority, then we will dequeue the highest-priority candidate along with every other candidate with the same
                // priority. Candidates that are not dequeued remain in their respective track queues.
                let priority = undefined;
                if (playingAlerts) {
                    for (let i = 0; i < playingAlerts.length; i++) {
                        const playing = playingAlerts[i];
                        if (priority === undefined || playing.definition.priority > priority) {
                            priority = playing.definition.priority;
                        }
                    }
                }
                while (nextAlerts.size > 0) {
                    const toPlay = nextAlerts.removeMin();
                    if (priority === undefined) {
                        priority = toPlay.definition.priority;
                    }
                    if (toPlay.definition.priority >= priority) {
                        priority = toPlay.definition.priority;
                        const track = entry.tracks.get(toPlay.track);
                        track.queue.removeMin();
                        if (!playingAlerts) {
                            playingAlerts = [];
                            this.playing.set(entry.name, playingAlerts);
                        }
                        playingAlerts.push(toPlay);
                        this.soundServerPublisher.pub('sound_server_interrupt', toPlay.packet, true, false);
                    }
                    else {
                        // The current candidate has a lower priority than the minimum required to be dequeued and played. Since we
                        // are evaluating candidates in order of decreasing priority, we are guaranteed that all remaining candidates
                        // have equal or lower priority than the current one. Therefore, we know no further candidates will be
                        // dequeued and can stop immediately.
                        break;
                    }
                }
            }
            else {
                for (const track of entry.tracks.values()) {
                    track.queue.clear();
                }
            }
        }
        /**
         * Deactivates an alert. Deactivating an alert will prevent queued activated instances of it from playing. In
         * addition, if an activated instance of the alert is currently playing and is continuous, it will be prevented from
         * looping.
         * @param id The (optionally suffixed) ID of the alert to deactivate.
         */
        deactivateAlert(id) {
            var _a;
            const deactivatedId = this.deactivateSuffix(id);
            if (deactivatedId === undefined) {
                return;
            }
            // Check if the deactivated ID is an alias by retrieving it from the alias map. If it's not in the map, then we
            // assume it is not an alias.
            const deactivatedUuid = (_a = this.activeAliasToUuid.get(deactivatedId)) !== null && _a !== void 0 ? _a : deactivatedId;
            // If the deactivated ID was an alias, remove it from the alias map.
            if (deactivatedUuid !== deactivatedId) {
                this.activeAliasToUuid.delete(deactivatedId);
            }
            const alertDef = this.registeredAlerts.get(deactivatedUuid);
            if (alertDef) {
                const playingAlerts = this.playing.get(alertDef.queue);
                if (playingAlerts) {
                    const playing = playingAlerts.find(query => query.id === deactivatedId);
                    if (playing && this.triggeredAlerts.get(deactivatedId) !== playing) {
                        this.soundServerPublisher.pub('sound_server_stop', playing.packet.key, true, false);
                    }
                }
            }
        }
        /**
         * Deactivates an alert suffix.
         * @param suffixedId The suffixed ID to deactivate.
         * @returns The ID of the alert that was deactivated as a result of deactivating the suffix, or `undefined` if no
         * alert was deactivated.
         */
        deactivateSuffix(suffixedId) {
            const id = this.activeSuffixedIdToId.get(suffixedId);
            if (id === undefined) {
                return undefined;
            }
            this.activeSuffixedIdToId.delete(suffixedId);
            const suffixedIds = this.idToActiveSuffixedIds.get(id);
            if (!suffixedIds) {
                this.activeAlerts.delete(id);
                return id;
            }
            else {
                suffixedIds.delete(suffixedId);
                if (suffixedIds.size === 0) {
                    this.activeAlerts.delete(id);
                    return id;
                }
                else {
                    return undefined;
                }
            }
        }
        /**
         * Untriggers an alert. Untriggering an alert will prevent queued triggered instances of it from playing. In
         * addition, if a triggered instance of the alert is currently playing and is continuous, it will be prevented from
         * looping.
         * @param uuid The (optionally suffixed) ID of the alert to untrigger.
         */
        untriggerAlert(uuid) {
            var _a;
            const untriggeredId = this.untriggerSuffix(uuid);
            if (untriggeredId === undefined) {
                return;
            }
            // Check if the untriggered ID is an alias by retrieving it from the alias map. If it's not in the map, then we
            // assume it is not an alias.
            const untriggeredUuid = (_a = this.triggeredAliasToUuid.get(untriggeredId)) !== null && _a !== void 0 ? _a : untriggeredId;
            // If the untriggered ID was an alias, remove it from the alias map.
            if (untriggeredUuid !== untriggeredId) {
                this.triggeredAliasToUuid.delete(untriggeredId);
            }
            const alertDef = this.registeredAlerts.get(untriggeredUuid);
            if (alertDef) {
                const playingAlerts = this.playing.get(alertDef.queue);
                if (playingAlerts) {
                    const playing = playingAlerts.find(query => query.id === untriggeredId);
                    if (playing && this.activeAlerts.get(untriggeredId) !== playing) {
                        this.soundServerPublisher.pub('sound_server_stop', playing.packet.key, true, false);
                    }
                }
            }
        }
        /**
         * Untriggers an alert suffix.
         * @param suffixedId The suffixed ID to untrigger.
         * @returns The ID of the alert that was untriggered as a result of deactivating the suffix, or `undefined` if no
         * alert was untriggered.
         */
        untriggerSuffix(suffixedId) {
            const id = this.triggeredSuffixedIdToId.get(suffixedId);
            if (id === undefined) {
                return undefined;
            }
            this.triggeredSuffixedIdToId.delete(suffixedId);
            const suffixedIds = this.idToTriggeredSuffixedIds.get(id);
            if (!suffixedIds) {
                this.triggeredAlerts.delete(id);
                return id;
            }
            else {
                suffixedIds.delete(suffixedId);
                if (suffixedIds.size === 0) {
                    this.triggeredAlerts.delete(id);
                    return id;
                }
                else {
                    return undefined;
                }
            }
        }
        /**
         * Kills an alert. Killing an alert will deactivate and untrigger it. In addition, if the alert is currently playing,
         * it will be stopped at the earliest opportunity.
         * @param uuid The (optionally suffixed) ID of the alert to kill.
         */
        killAlert(uuid) {
            var _a, _b;
            const deactivatedId = this.deactivateSuffix(uuid);
            const untriggeredId = this.untriggerSuffix(uuid);
            let deactivatedUuid = undefined;
            let untriggeredUuid = undefined;
            if (deactivatedId !== undefined) {
                // Check if the deactivated ID is an alias by retrieving it from the alias map. If it's not in the map, then we
                // assume it is not an alias.
                deactivatedUuid = (_a = this.activeAliasToUuid.get(deactivatedId)) !== null && _a !== void 0 ? _a : deactivatedId;
                // If the deactivated ID was an alias, remove it from the alias map.
                if (deactivatedUuid !== deactivatedId) {
                    this.activeAliasToUuid.delete(deactivatedId);
                }
            }
            if (untriggeredId !== undefined) {
                // Check if the untriggered ID is an alias by retrieving it from the alias map. If it's not in the map, then we
                // assume it is not an alias.
                untriggeredUuid = (_b = this.triggeredAliasToUuid.get(untriggeredId)) !== null && _b !== void 0 ? _b : untriggeredId;
                // If the untriggered ID was an alias, remove it from the alias map.
                if (untriggeredUuid !== untriggeredId) {
                    this.triggeredAliasToUuid.delete(untriggeredId);
                }
            }
            const deactivatedAlertDef = deactivatedUuid === undefined ? undefined : this.registeredAlerts.get(deactivatedUuid);
            const untriggeredAlertDef = untriggeredUuid === undefined ? undefined : this.registeredAlerts.get(untriggeredUuid);
            let killedPacketKey = undefined;
            if (deactivatedAlertDef) {
                const playingAlerts = this.playing.get(deactivatedAlertDef.queue);
                if (playingAlerts) {
                    const playing = playingAlerts.find(query => query.id === deactivatedId);
                    if (playing && this.triggeredAlerts.get(deactivatedId) !== playing) {
                        this.soundServerPublisher.pub('sound_server_kill', playing.packet.key, true, false);
                        killedPacketKey = playing.packet.key;
                    }
                }
            }
            if (untriggeredAlertDef) {
                const playingAlerts = this.playing.get(untriggeredAlertDef.queue);
                if (playingAlerts) {
                    const playing = playingAlerts.find(query => query.id === untriggeredId);
                    if (playing && this.activeAlerts.get(untriggeredId) !== playing && killedPacketKey !== playing.packet.key) {
                        this.soundServerPublisher.pub('sound_server_kill', playing.packet.key, true, false);
                    }
                }
            }
        }
        /**
         * Deactivates all alerts. This will stop all queued activated alerts from playing and prevent all currently playing
         * activated continuous alerts from looping.
         */
        deactivateAllAlerts() {
            this.activeAlerts.clear();
            this.activeAliasToUuid.clear();
            this.activeSuffixedIdToId.clear();
            this.idToActiveSuffixedIds.clear();
            for (const playingAlerts of this.playing.values()) {
                for (const playing of playingAlerts) {
                    if (this.triggeredAlerts.get(playing.id) !== playing) {
                        this.soundServerPublisher.pub('sound_server_stop', playing.packet.key, true, false);
                    }
                }
            }
        }
        /**
         * Untriggers all alerts. This will stop all queued triggered alerts from playing and prevent all currently playing
         * triggered continuous alerts from looping.
         */
        untriggerAllAlerts() {
            this.triggeredAlerts.clear();
            this.triggeredAliasToUuid.clear();
            this.triggeredSuffixedIdToId.clear();
            this.idToTriggeredSuffixedIds.clear();
            for (const playingAlerts of this.playing.values()) {
                for (const playing of playingAlerts) {
                    if (this.activeAlerts.get(playing.id) !== playing) {
                        this.soundServerPublisher.pub('sound_server_stop', playing.packet.key, true, false);
                    }
                }
            }
        }
        /**
         * Kills all alerts. This deactivates and untriggers all alerts, stopping all queued alerts from playing and stopping
         * all currently playing alerts at the earliest opportunity.
         */
        killAllAlerts() {
            this.activeAlerts.clear();
            this.triggeredAlerts.clear();
            this.activeAliasToUuid.clear();
            this.triggeredAliasToUuid.clear();
            this.activeSuffixedIdToId.clear();
            this.idToActiveSuffixedIds.clear();
            this.triggeredSuffixedIdToId.clear();
            this.idToTriggeredSuffixedIds.clear();
            for (const playingAlerts of this.playing.values()) {
                for (const playing of playingAlerts) {
                    this.soundServerPublisher.pub('sound_server_kill', playing.packet.key, true, false);
                }
            }
        }
        /**
         * Responds to when a sound packet stops playing.
         * @param key The key of the stopped packet.
         */
        onPacketEnded(key) {
            const queueName = this.packetKeyToQueueMap.get(key);
            if (queueName === undefined) {
                return;
            }
            const queueEntry = this.queues.get(queueName);
            if (!queueEntry) {
                this.packetKeyToQueueMap.delete(key);
                return;
            }
            const playingAlerts = this.playing.get(queueName);
            if (playingAlerts) {
                const finishedAlertIndex = playingAlerts.findIndex(alert => alert.packet.key === key);
                const finishedAlert = playingAlerts[finishedAlertIndex];
                if (finishedAlert) {
                    // Check if the alert that finished playing was triggered. If so, remove the alert from the triggered list as well
                    // as all of its triggered suffixes.
                    if (this.triggeredAlerts.get(finishedAlert.id) === finishedAlert) {
                        this.triggeredAlerts.delete(finishedAlert.id);
                        if (finishedAlert.id !== finishedAlert.definition.uuid) {
                            this.triggeredAliasToUuid.delete(finishedAlert.id);
                        }
                        const suffixedIds = this.idToTriggeredSuffixedIds.get(finishedAlert.id);
                        if (suffixedIds) {
                            for (const suffixedId of suffixedIds) {
                                this.triggeredSuffixedIdToId.delete(suffixedId);
                            }
                            this.idToTriggeredSuffixedIds.delete(finishedAlert.id);
                        }
                    }
                    // Requeue the alert if it is repeatable and is still active.
                    if (finishedAlert.repeat && this.activeAlerts.get(finishedAlert.id) === finishedAlert) {
                        this.queueAlert(finishedAlert);
                    }
                    playingAlerts.splice(finishedAlertIndex, 1);
                }
            }
            queueEntry.debounceTimer.clear();
            this.dequeueAlert(queueEntry);
        }
        /**
         * Responds to when the wake state of the sound server changes.
         * @param isAwake Whether the sound server is awake.
         */
        onSoundServerWakeChanged(isAwake) {
            if (this.isSoundServerAwake === isAwake) {
                return;
            }
            this.isSoundServerAwake = isAwake;
            this.updateActiveState();
            if (!isAwake) {
                // If the sound server is asleep, it will automatically stop playing all sound packets. Additionally, any
                // commands we sent to the server to start playing packets while the server was asleep but before we got the
                // notification were ignored (so we will never receive a packet ended event for them). Therefore, we need to
                // clear all playing alerts to ensure that alerts don't get stuck in the 'playing' state.
                for (const playingAlerts of this.playing.values()) {
                    playingAlerts.length = 0;
                }
            }
        }
        /**
         * Updates this system's activity state. This system is considered active if both it and the sound server are awake.
         * On activation, all active continuous or repeating alerts are re-queued to play. On deactivation, all triggered and
         * queued alerts are cleared and all currently playing alerts are stopped at the earliest opportunity.
         */
        updateActiveState() {
            const isActive = this.isAwake && this.isSoundServerAwake;
            if (isActive === this.isActive) {
                return;
            }
            this.isActive = isActive;
            if (isActive) {
                // Find all active alerts that are repeatable or continuous and re-queue them.
                for (const alert of this.activeAlerts.values()) {
                    if (alert.repeat || alert.packet.continuous) {
                        this.queueAlert(alert);
                    }
                }
            }
            else {
                // Clears all triggered alerts.
                this.triggeredAlerts.clear();
                this.triggeredAliasToUuid.clear();
                this.triggeredSuffixedIdToId.clear();
                this.idToTriggeredSuffixedIds.clear();
                // Clear all queued alerts.
                for (const queueEntry of this.queues.values()) {
                    queueEntry.debounceTimer.clear();
                    for (const track of queueEntry.tracks.values()) {
                        track.queue.clear();
                    }
                }
                // If the sound server is awake, kill all alerts that are currently playing. If the sound server is asleep, it
                // will stop all packets automatically and will not respond to commands, so there is no need to send the kill
                // commands.
                if (this.isSoundServerAwake) {
                    for (const playingAlerts of this.playing.values()) {
                        for (const playing of playingAlerts) {
                            this.soundServerPublisher.pub('sound_server_kill', playing.packet.key, true, false);
                        }
                    }
                }
            }
        }
        /**
         * Creates a sound packet key for a queue track.
         * @param queueName The name of the queue.
         * @param trackName The name of track.
         * @returns A sound packet key for the specified queue track.
         */
        static createPacketKey(queueName, trackName) {
            return `$$aural-alert-system-queue-${queueName}${trackName === '' ? '' : `-${trackName}`}$$`;
        }
    }
    AuralAlertSystem.ALERT_COMPARATOR = (a, b) => {
        const priorityDiff = b.definition.priority - a.definition.priority;
        // If same priority, bias toward older alerts so that repeatable alerts of the same priority don't constantly
        // preempt one another.
        if (priorityDiff === 0) {
            return a.timestamp - b.timestamp;
        }
        else {
            return priorityDiff;
        }
    };

    /**
     * Adapts {@link Warning} to {@link AuralAlertSystem}. Given a list of warnings, the adapter will register one aural
     * alert for each warning that defines a `soundId`. Alerts are set to repeat while active unless the warning is defined
     * to play only once. The adapter also manages activation/deactivation of the alerts using the condition logic of their
     * associated warnings. All alerts are assigned to the same queue.
     */
    class AuralAlertSystemWarningAdapter {
        /**
         * Creates a new instance of AuralAlertSystemWarningAdapter.
         * @param bus The event bus.
         * @param logicHost The XML logic host used to run this adapter's warning condition logic.
         * @param warnings This adapter's warnings, in order of decreasing priority.
         * @param queue The aural alert queue to assign this adapter's alerts.
         */
        constructor(bus, logicHost, warnings, queue) {
            this.bus = bus;
            this.logicHost = logicHost;
            this.warnings = warnings;
            this.queue = queue;
            this.publisher = this.bus.getPublisher();
            this.auralRegistrationManager = new AuralAlertRegistrationManager(bus);
        }
        /**
         * Registers this adapter's alerts with {@link AuralAlertSystem} and starts automatically managing alert states.
         */
        start() {
            var _a;
            for (let i = 0; i < this.warnings.length; i++) {
                const warning = this.warnings[i];
                if (warning.soundId === undefined) {
                    continue;
                }
                const uuid = UUID.GenerateUuid();
                this.auralRegistrationManager.register({
                    uuid,
                    queue: this.queue,
                    priority: -i,
                    sequence: warning.soundId,
                    continuous: false,
                    repeat: !((_a = warning.once) !== null && _a !== void 0 ? _a : false)
                });
                this.logicHost.addLogicAsNumber(warning.condition, value => {
                    if (value === 0) {
                        this.publisher.pub('aural_alert_deactivate', uuid, true, false);
                    }
                    else {
                        this.publisher.pub('aural_alert_activate', uuid, true, false);
                    }
                }, 0);
            }
        }
    }

    /**
     * Binds the state of an aural alert to the displayed state of a CAS alert and transports the aural alert state to
     * {@link AuralAlertSystem}.
     */
    class CasAuralAlertTransporter {
        /**
         * Creates a new instance of CasAuralAlertTransporter.
         * @param bus The event bus.
         * @param options Options with which to configure the transporter.
         * @param casSystem The CAS system. If not defined, the initialization of the aural alert's state cannot be
         * guaranteed to be correct unless the transporter is created before the CAS alert can be activated.
         * @returns A new instance of CasAuralAlertTransporter.
         */
        constructor(bus, options, casSystem) {
            var _a;
            this.publisher = bus.getPublisher();
            ({
                auralUuid: this.auralUuid,
                auralActivation: this.auralActivation,
                casUuid: this.casUuid,
                casPriority: this.casPriority,
                casSuffix: this.casSuffix,
                includeAcknowledged: this.includeAcknowledged,
            } = options);
            if (options.auralAction === 'trigger') {
                this.onAction = 'aural_alert_trigger';
                this.offAction = 'aural_alert_untrigger';
            }
            else {
                this.onAction = 'aural_alert_activate';
                this.offAction = 'aural_alert_deactivate';
            }
            const sub = bus.getSubscriber();
            if (options.auralAction !== 'trigger' && casSystem) {
                // Find out of the bound CAS alert is already displayed
                const filter = this.casSuffix === undefined
                    ? message => {
                        return message.uuid === this.casUuid
                            && message.priority === this.casPriority
                            && (message.suffixes === undefined || message.suffixes.length === 0)
                            && (this.includeAcknowledged || !message.acknowledged);
                    }
                    : message => {
                        return message.uuid === this.casUuid
                            && message.priority === this.casPriority
                            && (message.suffixes !== undefined && message.suffixes.includes(this.casSuffix))
                            && (this.includeAcknowledged || (message.acknowledgedSuffixes !== undefined && message.acknowledgedSuffixes.includes(this.casSuffix)));
                    };
                if (casSystem.casActiveMessageSubject.getArray().find(filter)) {
                    this.publisher.pub(this.onAction, (_a = (this.auralActivation && this.auralActivation())) !== null && _a !== void 0 ? _a : this.auralUuid, true, false);
                }
            }
            this.subs = [
                sub.on('cas_alert_displayed').handle(this.onAlertDisplayed.bind(this)),
                sub.on('cas_alert_hidden').handle(this.onAlertHidden.bind(this))
            ];
            if (!this.includeAcknowledged) {
                this.subs.push(sub.on('cas_alert_acknowledged').handle(this.onAlertAcknowledged.bind(this)));
            }
        }
        /**
         * Responds to when a CAS alert is displayed.
         * @param alert Data describing the displayed alert.
         */
        onAlertDisplayed(alert) {
            var _a;
            if (alert.uuid === this.casUuid && alert.priority === this.casPriority && alert.suffix === this.casSuffix && (this.includeAcknowledged || !alert.acknowledged)) {
                this.publisher.pub(this.onAction, (_a = (this.auralActivation && this.auralActivation())) !== null && _a !== void 0 ? _a : this.auralUuid, true, false);
            }
        }
        /**
         * Responds to when a CAS alert is hidden.
         * @param alert Data describing the hidden alert.
         */
        onAlertHidden(alert) {
            if (alert.uuid === this.casUuid && alert.priority === this.casPriority && alert.suffix === this.casSuffix) {
                this.publisher.pub(this.offAction, this.auralUuid, true, false);
            }
        }
        /**
         * Responds to when a CAS alert is acknowledged.
         * @param alert Data describing the acknowledged alert.
         */
        onAlertAcknowledged(alert) {
            // This method is only called if includeAcknowledged is false, so if the alert matches we always want to deactivate the aural.
            if (alert.uuid === this.casUuid && alert.priority === this.casPriority && alert.suffix === this.casSuffix) {
                this.publisher.pub('aural_alert_deactivate', this.auralUuid, true, false);
            }
        }
        /**
         * Destroys this transporter. Once destroyed, it will no longer automatically manage the state of its aural alert.
         */
        destroy() {
            this.subs.forEach(sub => { sub.destroy(); });
        }
        /**
         * Creates a new instance of CasAuralAlertTransporter, which will automatically activate and deactivate an aural
         * alert based on whether a bound CAS alert is being displayed as a message.
         * @param bus The event bus.
         * @param options Options with which to configure the transporter.
         * @param casSystem The CAS system. If not defined, the transporter should be created before its bound CAS alert
         * can be activated. Otherwise the initialization of the aural alert's state cannot be guaranteed to be correct.
         * @returns A new instance of CasAuralAlertTransporter.
         */
        static create(bus, options, casSystem) {
            return new CasAuralAlertTransporter(bus, options, casSystem);
        }
    }

    /**
     * A default implementation of {@link XmlAuralAlertParser}.
     */
    class DefaultXmlAuralAlertParser {
        /**
         * Creates a new instance of DefaultXmlAuralAlertParser.
         * @param baseInstrument The BaseInstrument.
         * @param defaultQueue The default queue to assign alerts if they do not explicitly define one themselves.
         */
        constructor(baseInstrument, defaultQueue) {
            this.baseInstrument = baseInstrument;
            this.defaultQueue = defaultQueue;
        }
        /** @inheritdoc */
        parse(root) {
            const results = [];
            for (const defElement of root.querySelectorAll(':scope>Alert')) {
                const def = this.parseAlertDefinition(this.baseInstrument, defElement);
                if (def) {
                    results.push(def);
                }
            }
            return results;
        }
        /**
         * Parses a single alert definition.
         * @param baseInstrument The BaseInstrument.
         * @param element The element defining the alert.
         * @returns The definition specified by the element, or `undefined` if one could not be parsed.
         */
        parseAlertDefinition(baseInstrument, element) {
            var _a, _b, _c, _d;
            const uuid = element.getAttribute('uuid');
            if (uuid === null || uuid.length === 0) {
                console.warn('AuralAlertSystemXmlAdapter: alert is missing "uuid" attribute. Discarding alert.');
                return undefined;
            }
            let queue = element.getAttribute('queue');
            if (queue === null || queue.length === 0) {
                queue = this.defaultQueue;
            }
            let priority;
            const priorityText = element.getAttribute('priority');
            if (priorityText) {
                priority = Number(priorityText);
                if (isNaN(priority)) {
                    console.warn('AuralAlertSystemXmlAdapter: unrecognized alert "priority" attribute (must be a number). Defaulting to 0.');
                    priority = 0;
                }
            }
            else {
                priority = 0;
            }
            const track = (_a = element.getAttribute('track')) !== null && _a !== void 0 ? _a : '';
            const sequence = this.parseSequence(element.querySelector(':scope>Sequence'));
            if (!sequence || sequence.length === 0) {
                return undefined;
            }
            let continuous;
            const continuousText = element.getAttribute('continuous');
            switch (continuousText === null || continuousText === void 0 ? void 0 : continuousText.toLowerCase()) {
                case 'true':
                    continuous = true;
                    break;
                case 'false':
                case undefined:
                    continuous = false;
                    break;
                default:
                    console.warn('AuralAlertSystemXmlAdapter: unrecognized alert "continuous" attribute (must be a boolean). Defaulting to false.');
                    continuous = false;
            }
            let repeat;
            const repeatText = element.getAttribute('requeue');
            switch (repeatText === null || repeatText === void 0 ? void 0 : repeatText.toLowerCase()) {
                case 'true':
                    repeat = true;
                    break;
                case 'false':
                case undefined:
                    repeat = false;
                    break;
                default:
                    console.warn('AuralAlertSystemXmlAdapter: unrecognized alert "repeat" attribute (must be a boolean). Defaulting to false.');
                    repeat = false;
            }
            let queuedLifetime;
            const queuedLifetimeText = element.getAttribute('queued-lifetime');
            if (queuedLifetimeText) {
                queuedLifetime = Number(queuedLifetimeText);
                if (isNaN(queuedLifetime)) {
                    console.warn('AuralAlertSystemXmlAdapter: unrecognized alert "queued-lifetime" attribute (must be a number). Defaulting to Infinity.');
                    queuedLifetime = undefined;
                }
            }
            else {
                queuedLifetime = undefined;
            }
            let logic = undefined;
            const conditionElement = element.querySelector(':scope>Condition');
            if (conditionElement) {
                logic = new CompositeLogicXMLElement(baseInstrument, conditionElement);
            }
            if (logic) {
                return {
                    type: 'logic',
                    uuid,
                    queue,
                    priority,
                    track,
                    sequence,
                    continuous,
                    repeat,
                    queuedLifetime,
                    logic
                };
            }
            else {
                const casElement = element.querySelector(':scope>CAS');
                if (casElement) {
                    const casUuid = casElement.getAttribute('uuid');
                    if (casUuid === null || casUuid.length === 0) {
                        console.warn('AuralAlertSystemXmlAdapter: alert CAS tag is missing "uuid" attribute. Discarding alert.');
                        return undefined;
                    }
                    let casPriority;
                    switch ((_b = casElement.getAttribute('type')) === null || _b === void 0 ? void 0 : _b.toLowerCase()) {
                        case 'warning':
                            casPriority = exports.AnnunciationType.Warning;
                            break;
                        case 'caution':
                            casPriority = exports.AnnunciationType.Caution;
                            break;
                        case 'advisory':
                            casPriority = exports.AnnunciationType.Advisory;
                            break;
                        case 'safeop':
                            casPriority = exports.AnnunciationType.SafeOp;
                            break;
                        default:
                            console.warn('AuralAlertSystemXmlAdapter: alert CAS tag has missing or unrecognized "type" attribute. Discarding alert.');
                            return undefined;
                    }
                    let casSuffix = (_c = casElement.getAttribute('suffix')) !== null && _c !== void 0 ? _c : undefined;
                    if (casSuffix && casSuffix.length === 0) {
                        casSuffix = undefined;
                    }
                    let casIncludeAcknowledged;
                    switch ((_d = casElement.getAttribute('acknowledged')) === null || _d === void 0 ? void 0 : _d.toLowerCase()) {
                        case 'true':
                            casIncludeAcknowledged = true;
                            break;
                        case 'false':
                            casIncludeAcknowledged = false;
                            break;
                        default:
                            console.warn('AuralAlertSystemXmlAdapter: alert CAS tag has missing or unrecognized "acknowledged" attribute. Discarding alert.');
                            return undefined;
                    }
                    return {
                        type: 'cas',
                        uuid,
                        queue,
                        priority,
                        track,
                        sequence,
                        continuous,
                        repeat,
                        queuedLifetime,
                        casUuid,
                        casPriority,
                        casSuffix,
                        casIncludeAcknowledged
                    };
                }
                else {
                    console.warn('AuralAlertSystemXmlAdapter: alert is missing both Condition and CAS child tags. Discarding alert.');
                    return undefined;
                }
            }
        }
        /**
         * Parses a sound atom sequence.
         * @param element The element defining the sequence.
         * @returns The sound atom sequence defined by the element, or `undefined` if one could not be parsed.
         */
        parseSequence(element) {
            var _a;
            return (_a = element === null || element === void 0 ? void 0 : element.textContent) === null || _a === void 0 ? void 0 : _a.split(',').map(str => str.trim()).filter(str => str.length > 0);
        }
    }
    /**
     * Adapts XML-defined aural alerts to {@link AuralAlertSystem}. Handles the registration of the alerts and management
     * of alert state.
     */
    class AuralAlertSystemXmlAdapter {
        /**
         * Creates a new instance of AuralAlertSystemXmlAdapter.
         * @param bus The event bus.
         * @param logicHost The XML logic host used to run this adapter's XML logic.
         * @param casSystem The CAS system.
         * @param alertDefsRoot The root XML document element containing the aural alert definitions to use.
         * @param parser The parser to use to parse alert definitions from the XML document.
         */
        constructor(bus, logicHost, casSystem, alertDefsRoot, parser) {
            this.bus = bus;
            this.logicHost = logicHost;
            this.casSystem = casSystem;
            this.publisher = this.bus.getPublisher();
            if (alertDefsRoot === null) {
                return;
            }
            const defs = parser.parse(alertDefsRoot);
            if (defs.length === 0) {
                return;
            }
            this.alertDefinitions = defs;
            this.auralRegistrationManager = new AuralAlertRegistrationManager(bus);
        }
        /**
         * Registers this adapter's alerts with {@link AuralAlertSystem} and starts automatically managing alert states.
         */
        start() {
            if (!this.auralRegistrationManager || !this.alertDefinitions) {
                return;
            }
            for (const def of this.alertDefinitions) {
                this.auralRegistrationManager.register({
                    uuid: def.uuid,
                    queue: def.queue,
                    priority: def.priority,
                    track: def.track,
                    sequence: def.sequence,
                    continuous: def.continuous,
                    repeat: def.repeat,
                    queuedLifetime: def.queuedLifetime
                });
                if (def.type === 'logic') {
                    this.logicHost.addLogicAsNumber(def.logic, value => {
                        if (value === 0) {
                            this.publisher.pub('aural_alert_deactivate', def.uuid, true, false);
                        }
                        else {
                            this.publisher.pub('aural_alert_activate', def.uuid, true, false);
                        }
                    }, 0);
                }
                else {
                    CasAuralAlertTransporter.create(this.bus, {
                        auralUuid: def.uuid,
                        auralAction: 'activate',
                        casUuid: def.casUuid,
                        casPriority: def.casPriority,
                        casSuffix: def.casSuffix,
                        includeAcknowledged: def.casIncludeAcknowledged,
                    }, this.casSystem);
                }
            }
        }
    }

    /**
     * A server which plays and manages sounds. Commands to start or stop playing sounds can be sent to the server via the
     * event bus.
     *
     * The server plays sounds as _packets_. Each sound packet consists of a string key and zero or more _sound atoms_ that
     * are played in sequence. Each sound atom represents a single playable sound file from within the sim. Sound packets
     * can be played as a one-shot or be looped continuously. Only one packet with a given key can be played at a time.
     * Queueing of packets with the same key is supported.
     */
    class SoundServer {
        /**
         * Creates a new instance of SoundServer. The server is initially awake after being created.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.bus = bus;
            this.controlSub = this.bus.getSubscriber();
            this.publisher = this.bus.getPublisher();
            this.active = new Map();
            this.queued = new Map();
            this.isAwake = true;
            this.publisher.pub('sound_server_initialized', false, true, true);
            this.publisher.pub('sound_server_is_awake', true, true, true);
            this.init();
        }
        /**
         * Waits until the sim has entered the in-game state and initializes this server. Once initialized, this server will
         * respond to commands over the event bus.
         */
        async init() {
            // Wait for game to reach "ingame" state. THEN wait a short duration and wait again for game to reach "ingame"
            // state because during load the game will go from loading -> ingame -> briefing.
            await Wait.awaitSubscribable(GameStateProvider.get(), state => state === GameState.ingame, true);
            await Wait.awaitDelay(500);
            await Wait.awaitSubscribable(GameStateProvider.get(), state => state === GameState.ingame, true);
            this.controlSub.on('sound_server_play').handle(this.playPacket.bind(this));
            this.controlSub.on('sound_server_queue').handle(this.queuePacket.bind(this));
            this.controlSub.on('sound_server_interrupt').handle(this.interruptPacket.bind(this));
            this.controlSub.on('sound_server_stop').handle(this.stopPacket.bind(this));
            this.controlSub.on('sound_server_kill').handle(this.killPacket.bind(this));
            this.controlSub.on('sound_server_stop_all').handle(this.stopAllPackets.bind(this));
            this.controlSub.on('sound_server_kill_all').handle(this.killAllPackets.bind(this));
            this.controlSub.on('sound_server_play_sound').handle(this.playSound.bind(this));
            this.controlSub.on('sound_server_start_sound').handle(this.startSound.bind(this));
            this.controlSub.on('sound_server_stop_sound').handle(this.stopPacket.bind(this));
            this.publisher.pub('sound_server_initialized', true, true, true);
        }
        /**
         * A callback method that responds to when the sim notifies the JS instrument that a sound file has stopped playing.
         * This method needs to be manually called for `SoundServer` to function properly.
         * @param id The ID of the sound file that stopped playing.
         */
        onSoundEnd(id) {
            // Create a copy of the active entries because the map can be mutated while we are iterating.
            const activeEntries = Array.from(this.active.values());
            for (let i = 0; i < activeEntries.length; i++) {
                const entry = activeEntries[i];
                const entrySoundPlaying = entry.sequence[entry.currentIndexPlaying];
                if (!entrySoundPlaying) {
                    this.cleanupPacket(entry);
                }
                else if (Name_Z.compare(entrySoundPlaying, id)) {
                    this.advancePacket(entry);
                }
            }
        }
        /**
         * Wakes this server. Once awake, this server will respond to commands.
         */
        wake() {
            if (this.isAwake) {
                return;
            }
            this.isAwake = true;
            this.publisher.pub('sound_server_is_awake', true, true, true);
        }
        /**
         * Puts this server to sleep. This will stop all currently playing sound packets at the earliest opportunity and
         * clears all queued packets. While asleep, this server will not respond to any commands.
         */
        sleep() {
            if (!this.isAwake) {
                return;
            }
            this.isAwake = false;
            this.queued.clear();
            this.publisher.pub('sound_server_is_awake', false, true, true);
        }
        /**
         * Plays a sound packet if and only if there is no currently playing packet with the same key.
         * @param packet The packet to play.
         */
        playPacket(packet) {
            if (!this.isAwake) {
                return;
            }
            if (!this.active.get(packet.key)) {
                this.queuePacket(packet);
            }
        }
        /**
         * Plays a sound packet at the earliest opportunity. If there is no currently playing packet with the same key, it
         * will begin playing immediately. Otherwise, all queued packets with the same key are cleared, the existing packet
         * will be stopped the next time one of its sound atoms finishes playing, and the new packet will begin playing at
         * that time.
         * @param packet The packet to play.
         */
        interruptPacket(packet) {
            if (!this.isAwake) {
                return;
            }
            const active = this.active.get(packet.key);
            if (active) {
                active.alive = false;
            }
            this.queued.delete(packet.key);
            this.queuePacket(packet);
        }
        /**
         * Queues a sound packet. The queued packet will immediately start playing if there is no currently playing packet
         * with the same key. Otherwise it will be queued to play once all currently playing and previously queued packets
         * with the same key have finished playing.
         * @param packet The packet to queue.
         */
        queuePacket(packet) {
            var _a;
            if (!this.isAwake) {
                return;
            }
            const sequence = SoundServer.getSoundSequence(packet);
            const entry = {
                key: packet.key,
                sequence,
                continuous: packet.continuous,
                currentIndexPlaying: 0,
                timeout: (_a = packet.timeout) !== null && _a !== void 0 ? _a : SoundServer.DEFAULT_TIMEOUT,
                alive: true,
                timer: new DebounceTimer(),
                timeoutCallback: () => { this.cleanupPacket(entry); }
            };
            if (this.active.get(packet.key)) {
                let queue = this.queued.get(entry.key);
                if (!queue) {
                    this.queued.set(entry.key, queue = []);
                }
                queue.push(entry);
            }
            else {
                this.startPacket(entry);
            }
        }
        /**
         * Stops a continuous packet from looping the next time its sequence finishes and clears any packets queued behind
         * it.
         * @param key The key of the packet to stop.
         */
        stopPacket(key) {
            if (!this.isAwake) {
                return;
            }
            this.queued.delete(key);
            const active = this.active.get(key);
            if (active) {
                active.continuous = false;
            }
        }
        /**
         * Stops a packet from playing the next time one of its sound atoms finishes playing and clears any packets queued
         * behind it.
         * @param key The key of the packet to kill.
         */
        killPacket(key) {
            if (!this.isAwake) {
                return;
            }
            this.queued.delete(key);
            const active = this.active.get(key);
            if (active) {
                active.alive = false;
            }
        }
        /**
         * Stops all currently playing continuous packets from looping the next time their sequences finish and clears all
         * queued packets.
         */
        stopAllPackets() {
            if (!this.isAwake) {
                return;
            }
            this.queued.clear();
            for (const entry of this.active.values()) {
                entry.continuous = false;
            }
        }
        /**
         * Stops all packets from playing the next time one of their sound atoms finishes playing and clears all queued
         * packets.
         */
        killAllPackets() {
            if (!this.isAwake) {
                return;
            }
            this.queued.clear();
            for (const entry of this.active.values()) {
                entry.alive = false;
            }
        }
        /**
         * Plays a non-continuous sound packet consisting of a single sound atom and whose key matches the ID of the sound
         * atom.
         * @param id The ID of the sound atom to play.
         */
        playSound(id) {
            this.playPacket({ key: id, sequence: id, continuous: false });
        }
        /**
         * Plays a continuous sound packet consisting of a single sound atom and whose key matches the ID of the sound atom.
         * @param id The ID of the sound atom to play.
         */
        startSound(id) {
            this.playPacket({ key: id, sequence: id, continuous: true });
        }
        /**
         * Starts playing a packet from the beginning of its sequence.
         * @param entry The entry of the packet to start playing.
         */
        startPacket(entry) {
            this.active.set(entry.key, entry);
            this.publisher.pub('sound_server_packet_started', entry.key, true, false);
            if (entry.sequence.length === 0) {
                this.cleanupPacket(entry);
            }
            else {
                Coherent.call('PLAY_INSTRUMENT_SOUND', entry.sequence[0].originalStr);
                entry.currentIndexPlaying = 0;
                entry.timer.schedule(entry.timeoutCallback, entry.timeout);
            }
        }
        /**
         * Attempts to advance a packet to the next sound atom in its sequence. If the end of the sequence is reached, the
         * packet will loop to the beginning if it is continuous. Otherwise it will finish playing.
         * @param entry The entry of the packet to advance.
         */
        advancePacket(entry) {
            if (!this.isAwake || !entry.alive) {
                this.cleanupPacket(entry);
                return;
            }
            let nextSoundToPlayIndex = entry.currentIndexPlaying + 1;
            if (nextSoundToPlayIndex >= entry.sequence.length) {
                // We have reached the end of the sequence. If the packet is continuous, loop to the beginning. Otherwise, end the packet.
                if (entry.continuous) {
                    nextSoundToPlayIndex = 0;
                    entry.timer.schedule(entry.timeoutCallback, entry.timeout);
                }
                else {
                    this.cleanupPacket(entry);
                    return;
                }
            }
            const soundToPlay = entry.sequence[nextSoundToPlayIndex];
            Coherent.call('PLAY_INSTRUMENT_SOUND', soundToPlay.originalStr);
            entry.currentIndexPlaying = nextSoundToPlayIndex;
        }
        /**
         * Finishes playing a packet and automatically starts playing the packet queued behind the finished packet, if one
         * exists.
         * @param entry The entry of the packet to finish.
         */
        cleanupPacket(entry) {
            entry.alive = false;
            entry.timer.clear();
            const deleted = this.active.delete(entry.key);
            if (!deleted) {
                return;
            }
            this.publisher.pub('sound_server_packet_ended', entry.key, true, false);
            // Dequeue the next packet, if any.
            const queue = this.queued.get(entry.key);
            if (queue) {
                const next = queue.shift();
                if (queue.length === 0) {
                    this.queued.delete(entry.key);
                }
                if (next) {
                    this.startPacket(next);
                }
            }
        }
        /**
         * Gets a sound atom sequence as an array of `Name_Z` objects from a sound packet.
         * @param packet The packet defining the sound atom sequence.
         * @returns The sound atom sequence defined by a packet, as an array of `Name_Z` objects.
         */
        static getSoundSequence(packet) {
            const sequence = [];
            if (typeof packet.sequence === 'string') {
                if (packet.sequence !== '') {
                    sequence.push(new Name_Z(packet.sequence));
                }
            }
            else {
                for (let i = 0; i < packet.sequence.length; i++) {
                    if (packet.sequence[i] !== '') {
                        sequence.push(new Name_Z(packet.sequence[i]));
                    }
                }
            }
            return sequence;
        }
    }
    SoundServer.DEFAULT_TIMEOUT = 10000;
    /**
     * A controller which provides a convenient interface with which to send commands to {@link SoundServer}.
     */
    class SoundServerController {
        /**
         * Creates a new instance of SoundServerController.
         * @param bus The event bus.
         */
        constructor(bus) {
            this.publisher = bus.getPublisher();
            this.subscriber = bus.getSubscriber();
        }
        /**
         * Waits for the sound server to finish initialization.
         * @returns A Promise which fulfills with a value of `true` when the sound server finishes initialization.
         */
        awaitInitialized() {
            return Wait.awaitConsumer(this.subscriber.on('sound_server_initialized'), init => init, true);
        }
        /**
         * Requests a sound packet to be played if there is no existing packet with the same key currently being played.
         * @param packet The sound packet to play.
         */
        play(packet) {
            this.publisher.pub('sound_server_play', packet, true, false);
        }
        /**
         * Requests a sound packet to be queued. If there is no existing packet with the same key currently being played, the
         * packet will begin playing immediately. Otherwise, the new packet will begin playing after the existing packet and
         * any other queued packets with the same key are finished playing.
         * @param packet The sound packet to queue.
         */
        queue(packet) {
            this.publisher.pub('sound_server_queue', packet, true, false);
        }
        /**
         * Requests a sound packet to be played at the earliest opportunity. If there is no existing packet with the same key
         * currently being played, the packet will begin playing immediately. Otherwise, the existing packet will be stopped
         * the next time one of its sound atoms finishes playing, any queued packets with the same key will be discarded, and
         * the new packet will begin playing at that time.
         * @param packet The sound packet to play.
         */
        interrupt(packet) {
            this.publisher.pub('sound_server_interrupt', packet, true, false);
        }
        /**
         * Requests that a continuous sound packet stop playing instead of looping the next time its sequence finishes. This
         * also prevents any queued packets with the same key from
         * playing.
         * @param key The key of the sound packet to stop.
         */
        stop(key) {
            this.publisher.pub('sound_server_stop', key, true, false);
        }
        /**
         * Requests that a sound packet stop playing at the earliest opportunity (the next time one of its sound atoms
         * finishes playing). This also prevents any queued packets with the same key from playing.
         * @param key The key of the sound packet to kill.
         */
        kill(key) {
            this.publisher.pub('sound_server_kill', key, true, false);
        }
        /**
         * Requests that all currently playing continuous sound packets stop playing instead of looping the next time their
         * sequences finish. This also clears all queued packets.
         */
        stopAll() {
            this.publisher.pub('sound_server_stop_all', undefined, true, false);
        }
        /**
         * Requests that all currently playing sound packets stop playing at the earliest opportunity (the next time one of
         * their sound atoms finishes playing). This also clears all queued packets.
         */
        killAll() {
            this.publisher.pub('sound_server_kill_all', undefined, true, false);
        }
        /**
         * Requests a single sound atom to be played non-continuously. Calling this method is an alias for calling
         * `play({ key: id, sequence: id, continuous: false })`.
         * @param id The ID of the sound atom to play.
         */
        playSound(id) {
            this.publisher.pub('sound_server_play_sound', id, true, false);
        }
        /**
         * Requests a single sound atom to be played continuously. Calling this method is an alias for calling
         * `play({ key: id, sequence: id, continuous: true })`.
         * @param id The ID of the sound atom to play.
         */
        startSound(id) {
            this.publisher.pub('sound_server_start_sound', id, true, false);
        }
    }

    /** Collection of string utility functions. */
    class StringUtils {
        /**
         * Replaces all `'0'` characters in a string with the slashed zero character.
         * @param text The string to convert.
         * @returns A string identical to the input string except with all `'0'` characters converted to the slashed zero
         * character.
         */
        static useZeroSlash(text) {
            return text === null || text === void 0 ? void 0 : text.replace(StringUtils.ZERO_REGEX, '0̸');
        }
        /**
         * Replaces all `'.'` characters in a string with the zero width decimal character.
         * @param text The string to convert.
         * @returns A string identical to the input string except with all `'.'` characters converted to the zero width decimal
         * character.
         */
        static useZeroWidthDecimal(text) {
            return text === null || text === void 0 ? void 0 : text.replace(StringUtils.DECIMAL_REGEX, StringUtils.ZERO_WIDTH_DECIMAL);
        }
        /**
         * Removes leading whitespace and line terminator characters from a string.
         * @param str The string to trim.
         * @returns A new string representing `str` with all leading whitespace and line terminator characters removed.
         */
        static trimStart(str) {
            return str.replace(StringUtils.TRIM_START_REGEX, '');
        }
        /**
         * Removes trailing whitespace and line terminator characters from a string.
         * @param str The string to trim.
         * @returns A new string representing `str` with all trailing whitespace and line terminator characters removed.
         */
        static trimEnd(str) {
            return str.replace(StringUtils.TRIM_END_REGEX, '');
        }
    }
    StringUtils.DIRECT_TO = 'Ð';
    StringUtils.DEGREE = '°';
    StringUtils.EN_DASH = '–';
    StringUtils.ZERO_WIDTH_DECIMAL = '·';
    StringUtils.RIGHT_POINTING_ISOSCELES_RIGHT_TRIANGLE = '🞂';
    StringUtils.LEFT_POINTING_ISOSCELES_RIGHT_TRIANGLE = '🞀';
    StringUtils.RIGHT_POINTING_TRIANGLE_CENTRED = '⯈';
    StringUtils.LEFT_POINTING_TRIANGLE_CENTRED = '⯇';
    StringUtils.UP_POINTING_TRIANGLE_CENTRED = '⯅';
    StringUtils.DOWN_POINTING_TRIANGLE_CENTRED = '⯆';
    StringUtils.ZERO_REGEX = /0/g;
    StringUtils.DECIMAL_REGEX = /\./g;
    StringUtils.TRIM_START_REGEX = /^\s+/;
    StringUtils.TRIM_END_REGEX = /\s+$/;

    /**
     * A component that lets you add and remove children.
     */
    class CollectionComponent extends DisplayComponent {
        constructor() {
            super(...arguments);
            this.items = new Array();
            this.itemCounter = 0;
        }
        /**
         * Add a new child element.
         * @param node A VNode to add to the container.
         * @returns An identifier for the added element.
         */
        addItem(node) {
            const id = `${this.props.id}_${this.itemCounter++}`;
            exports.FSComponent.render(exports.FSComponent.buildComponent("div", { id: id }, node), document.getElementById(this.props.id));
            return id;
        }
        /**
         * Remove a child element.
         * @param id The ID of the child to remove.
         */
        removeItem(id) {
            exports.FSComponent.remove(document.getElementById(id));
        }
        /**
         * Insert a child before an element.
         * @param node The node to insert
         * @param before The element to insert before.
         * @returns An identifier for the added element.
         */
        insertBefore(node, before) {
            const id = `${this.props.id}_${this.itemCounter++}`;
            exports.FSComponent.renderBefore(exports.FSComponent.buildComponent("div", { id: id }, node), document.getElementById(before));
            return id;
        }
        /**
         * Insert a child after an element.
         * @param node The node to insert
         * @param after The element to insert after.
         * @returns An identifier for the added element.
         */
        insertAfter(node, after) {
            const id = `${this.props.id}_${this.itemCounter++}`;
            exports.FSComponent.renderAfter(exports.FSComponent.buildComponent("div", { id: id }, node), document.getElementById(after));
            return id;
        }
        /**
         * Render the element.
         * @returns A VNode.
         */
        render() {
            return (exports.FSComponent.buildComponent("div", { id: this.props.id }));
        }
    }

    /**
     * A class that wraps the actual instrumenet implementation and handles the sim's vcockpit lifecycle.
     */
    class FsBaseInstrument extends BaseInstrument {
        /**
         * A callback called when the element is attached to the DOM.
         */
        connectedCallback() {
            super.connectedCallback();
            this.fsInstrument = this.constructInstrument();
        }
        /**
         * Update method called by BaseInstrument
         */
        Update() {
            super.Update();
            if (this.fsInstrument) {
                this.fsInstrument.Update();
            }
        }
        /** @inheritdoc */
        onInteractionEvent(_args) {
            if (this.fsInstrument) {
                this.fsInstrument.onInteractionEvent(_args);
            }
        }
        /** @inheritdoc */
        onGameStateChanged(oldState, newState) {
            super.onGameStateChanged(oldState, newState);
            if (this.fsInstrument) {
                this.fsInstrument.onGameStateChanged(oldState, newState);
            }
        }
        /** @inheritdoc */
        onFlightStart() {
            super.onFlightStart();
            if (this.fsInstrument) {
                this.fsInstrument.onFlightStart();
            }
        }
        /** @inheritdoc */
        onSoundEnd(soundEventId) {
            super.onSoundEnd(soundEventId);
            if (this.fsInstrument) {
                this.fsInstrument.onSoundEnd(soundEventId);
            }
        }
        /**
         * Whether or not the instrument is interactive (a touchscreen instrument).
         * @returns True
         */
        get isInteractive() {
            return false;
        }
    }

    exports.APAltCapDirector = APAltCapDirector;
    exports.APAltDirector = APAltDirector;
    exports.APBackCourseDirector = APBackCourseDirector;
    exports.APFLCDirector = APFLCDirector;
    exports.APFPADirector = APFPADirector;
    exports.APGPDirector = APGPDirector;
    exports.APGSDirector = APGSDirector;
    exports.APGpsSteerDirector = APGpsSteerDirector;
    exports.APHdgDirector = APHdgDirector;
    exports.APHdgHoldDirector = APHdgHoldDirector;
    exports.APLvlDirector = APLvlDirector;
    exports.APNavDirector = APNavDirector;
    exports.APNoneLateralDirector = APNoneLateralDirector;
    exports.APNoneVerticalDirector = APNoneVerticalDirector;
    exports.APPitchDirector = APPitchDirector;
    exports.APPitchLvlDirector = APPitchLvlDirector;
    exports.APRadioNavInstrument = APRadioNavInstrument;
    exports.APRollDirector = APRollDirector;
    exports.APRollSteerDirector = APRollSteerDirector;
    exports.APStateManager = APStateManager;
    exports.APTogaPitchDirector = APTogaPitchDirector;
    exports.APTrkDirector = APTrkDirector;
    exports.APTrkHoldDirector = APTrkHoldDirector;
    exports.APVNavPathDirector = APVNavPathDirector;
    exports.APVSDirector = APVSDirector;
    exports.AbstractAutothrottle = AbstractAutothrottle;
    exports.AbstractFlightPathLegCalculator = AbstractFlightPathLegCalculator;
    exports.AbstractFlightPathLegRenderer = AbstractFlightPathLegRenderer;
    exports.AbstractFlightPathPlanRenderer = AbstractFlightPathPlanRenderer;
    exports.AbstractFmcPage = AbstractFmcPage;
    exports.AbstractFmcPageExtension = AbstractFmcPageExtension;
    exports.AbstractMapTextLabel = AbstractMapTextLabel;
    exports.AbstractMapTrafficIntruderIcon = AbstractMapTrafficIntruderIcon;
    exports.AbstractMapWaypointIcon = AbstractMapWaypointIcon;
    exports.AbstractNearestSubscription = AbstractNearestSubscription;
    exports.AbstractNumberUnitDisplay = AbstractNumberUnitDisplay;
    exports.AbstractSubscribable = AbstractSubscribable;
    exports.AbstractSubscribableArray = AbstractSubscribableArray;
    exports.AbstractSubscribableMap = AbstractSubscribableMap;
    exports.AbstractSubscribableSet = AbstractSubscribableSet;
    exports.AbstractTcasIntruder = AbstractTcasIntruder;
    exports.AbstractTransformingPathStream = AbstractTransformingPathStream;
    exports.AbstractUnit = AbstractUnit;
    exports.AbstractWaypoint = AbstractWaypoint;
    exports.AccelerometerPublisher = AccelerometerPublisher;
    exports.AccessibleUtils = AccessibleUtils;
    exports.AdaptiveNearestContext = AdaptiveNearestContext;
    exports.AdaptiveNearestSubscription = AdaptiveNearestSubscription;
    exports.AdcPublisher = AdcPublisher;
    exports.Adsb = Adsb;
    exports.AeroMath = AeroMath;
    exports.AffineTransformPathStream = AffineTransformPathStream;
    exports.AhrsPublisher = AhrsPublisher;
    exports.AiPilotPublisher = AiPilotPublisher;
    exports.AiracCycleFormatter = AiracCycleFormatter;
    exports.AiracUtils = AiracUtils;
    exports.AircraftInertialPublisher = AircraftInertialPublisher;
    exports.AirportUtils = AirportUtils;
    exports.AirspaceSearcher = AirspaceSearcher;
    exports.AirwayObject = AirwayObject;
    exports.AliasedUserSettingManager = AliasedUserSettingManager;
    exports.AltitudeLegCalculator = AltitudeLegCalculator;
    exports.AltitudeSelectManager = AltitudeSelectManager;
    exports.AmbientPublisher = AmbientPublisher;
    exports.Animator = Animator;
    exports.Annunciation = Annunciation;
    exports.AntiIcePublisher = AntiIcePublisher;
    exports.ApproachUtils = ApproachUtils;
    exports.ArcToFixLegCalculator = ArcToFixLegCalculator;
    exports.ArcTurnController = ArcTurnController;
    exports.ArraySubject = ArraySubject;
    exports.ArrayTaskQueue = ArrayTaskQueue;
    exports.ArrayUtils = ArrayUtils;
    exports.AuralAlertRegistrationManager = AuralAlertRegistrationManager;
    exports.AuralAlertSystem = AuralAlertSystem;
    exports.AuralAlertSystemWarningAdapter = AuralAlertSystemWarningAdapter;
    exports.AuralAlertSystemXmlAdapter = AuralAlertSystemXmlAdapter;
    exports.Autopilot = Autopilot;
    exports.AutopilotDriver = AutopilotDriver;
    exports.AutopilotInstrument = AutopilotInstrument;
    exports.AutothrottleSpeedTargetRecord = AutothrottleSpeedTargetRecord;
    exports.AutothrottleThrottle = AutothrottleThrottle;
    exports.AvionicsPlugin = AvionicsPlugin;
    exports.BacklightLevelController = BacklightLevelController;
    exports.BaseInstrumentPublisher = BaseInstrumentPublisher;
    exports.BasePublisher = BasePublisher;
    exports.BasicAvionicsSystem = BasicAvionicsSystem;
    exports.BasicConsumer = BasicConsumer;
    exports.BasicFacilityWaypoint = BasicFacilityWaypoint;
    exports.BasicNavAngleSubject = BasicNavAngleSubject;
    exports.BasicNavAngleUnit = BasicNavAngleUnit;
    exports.BinaryHeap = BinaryHeap;
    exports.BingComponent = BingComponent;
    exports.BinomialHeap = BinomialHeap;
    exports.BitFlags = BitFlags;
    exports.BrakeSimvarPublisher = BrakeSimvarPublisher;
    exports.CasAlertTransporter = CasAlertTransporter;
    exports.CasAuralAlertTransporter = CasAuralAlertTransporter;
    exports.CasRegistrationManager = CasRegistrationManager;
    exports.CasSystem = CasSystem;
    exports.CasSystemLegacyAdapter = CasSystemLegacyAdapter;
    exports.CdiUtils = CdiUtils;
    exports.ChartView = ChartView;
    exports.ChartsClient = ChartsClient;
    exports.ChecklistController = ChecklistController;
    exports.ChecklistDOMParser = ChecklistDOMParser;
    exports.ChecklistManager = ChecklistManager;
    exports.CircleInterceptLegCalculator = CircleInterceptLegCalculator;
    exports.CircleInterceptVectorBuilder = CircleInterceptVectorBuilder;
    exports.CircleVectorBuilder = CircleVectorBuilder;
    exports.ClippedPathStream = ClippedPathStream;
    exports.Clock = Clock;
    exports.ClockPublisher = ClockPublisher;
    exports.CollectionComponent = CollectionComponent;
    exports.ColorUtils = ColorUtils;
    exports.CombinedSubject = CombinedSubject;
    exports.ComponentProps = ComponentProps;
    exports.CompositeLogicXMLHost = CompositeLogicXMLHost;
    exports.CompoundUnit = CompoundUnit;
    exports.ComputedSubject = ComputedSubject;
    exports.ConfigBuilder = ConfigBuilder;
    exports.ConfigParser = ConfigParser;
    exports.ConnectCirclesVectorBuilder = ConnectCirclesVectorBuilder;
    exports.ConsumerSubject = ConsumerSubject;
    exports.ConsumerValue = ConsumerValue;
    exports.Context = Context;
    exports.ControlPublisher = ControlPublisher;
    exports.ControlSurfacesPublisher = ControlSurfacesPublisher;
    exports.CourseToAltitudeLegCalculator = CourseToAltitudeLegCalculator;
    exports.CourseToFixLegCalculator = CourseToFixLegCalculator;
    exports.CssMatrixTransform = CssMatrixTransform;
    exports.CssRotate3dTransform = CssRotate3dTransform;
    exports.CssRotateTransform = CssRotateTransform;
    exports.CssScale3dTransform = CssScale3dTransform;
    exports.CssScaleTransform = CssScaleTransform;
    exports.CssScaleXTransform = CssScaleXTransform;
    exports.CssScaleYTransform = CssScaleYTransform;
    exports.CssScaleZTransform = CssScaleZTransform;
    exports.CssTransformBuilder = CssTransformBuilder;
    exports.CssTransformChain = CssTransformChain;
    exports.CssTransformSubject = CssTransformSubject;
    exports.CssTranslate3dTransform = CssTranslate3dTransform;
    exports.CssTranslateTransform = CssTranslateTransform;
    exports.CssTranslateXTransform = CssTranslateXTransform;
    exports.CssTranslateYTransform = CssTranslateYTransform;
    exports.CssTranslateZTransform = CssTranslateZTransform;
    exports.CustomFlightPathLegRenderer = CustomFlightPathLegRenderer;
    exports.CustomWaypoint = CustomWaypoint;
    exports.DataInterface = DataInterface;
    exports.DateTimeFormatter = DateTimeFormatter;
    exports.DebounceTimer = DebounceTimer;
    exports.DefaultChecklistStateProvider = DefaultChecklistStateProvider;
    exports.DefaultFacilityWaypointCache = DefaultFacilityWaypointCache;
    exports.DefaultLNavComputerDataProvider = DefaultLNavComputerDataProvider;
    exports.DefaultLodBoundaryCache = DefaultLodBoundaryCache;
    exports.DefaultMapLabeledRingLabel = DefaultMapLabeledRingLabel;
    exports.DefaultMapSystemContext = DefaultMapSystemContext;
    exports.DefaultTcasAdvisoryDataProvider = DefaultTcasAdvisoryDataProvider;
    exports.DefaultTcasIntruder = DefaultTcasIntruder;
    exports.DefaultUserSettingManager = DefaultUserSettingManager;
    exports.DefaultXmlAuralAlertParser = DefaultXmlAuralAlertParser;
    exports.DeltaPacer = DeltaPacer;
    exports.DigitScroller = DigitScroller;
    exports.DirectToFixLegCalculator = DirectToFixLegCalculator;
    exports.DirectToPointVectorBuilder = DirectToPointVectorBuilder;
    exports.DiscontinuityLegCalculator = DiscontinuityLegCalculator;
    exports.DisplayComponent = DisplayComponent;
    exports.DisplayField = DisplayField;
    exports.DmeInterceptLegCalculator = DmeInterceptLegCalculator;
    exports.DmsFormatter = DmsFormatter;
    exports.DmsFormatter2 = DmsFormatter2;
    exports.DurationDisplay = DurationDisplay;
    exports.DurationFormatter = DurationFormatter;
    exports.EISPublisher = EISPublisher;
    exports.Easing = Easing;
    exports.EditableField = EditableField;
    exports.ElectricalPublisher = ElectricalPublisher;
    exports.EmptyDirector = EmptyDirector;
    exports.EventBus = EventBus;
    //PM Modif
    exports.EventBusListenerSync = EventBusListenerSync;
    //End PM Modif
    exports.EventRepublisher = EventRepublisher;
    exports.EventSubscriber = EventSubscriber;
    exports.ExpSmoother = ExpSmoother;
    exports.FacilityLoader = FacilityLoader;
    exports.FacilityRepository = FacilityRepository;
    exports.FacilityTypeSearchType = FacilityTypeSearchType;
    exports.FacilityUtils = FacilityUtils;
    exports.FacilityWaypointUtils = FacilityWaypointUtils;
    exports.FilteredMapSubject = FilteredMapSubject;
    exports.FilteredMappedSubscribableArray = FilteredMappedSubscribableArray;
    exports.FixToAltitudeLegCalculator = FixToAltitudeLegCalculator;
    exports.FixToDmeLegCalculator = FixToDmeLegCalculator;
    exports.FixToManualLegCalculator = FixToManualLegCalculator;
    exports.FlightPathCalculator = FlightPathCalculator;
    exports.FlightPathLegLineRenderer = FlightPathLegLineRenderer;
    exports.FlightPathLegPatternRenderer = FlightPathLegPatternRenderer;
    exports.FlightPathLegToLegCalculator = FlightPathLegToLegCalculator;
    exports.FlightPathRenderStyle = FlightPathRenderStyle;
    exports.FlightPathUtils = FlightPathUtils;
    exports.FlightPathVectorLineRenderer = FlightPathVectorLineRenderer;
    exports.FlightPathVectorPatternRenderer = FlightPathVectorPatternRenderer;
    exports.FlightPathWaypoint = FlightPathWaypoint;
    exports.FlightPlan = FlightPlan;
    exports.FlightPlanDisplayBuilder = FlightPlanDisplayBuilder;
    exports.FlightPlanLegIterator = FlightPlanLegIterator;
    exports.FlightPlanPredictor = FlightPlanPredictor;
    exports.FlightPlanPredictorUtils = FlightPlanPredictorUtils;
    exports.FlightPlanRouteManager = FlightPlanRouteManager;
    exports.FlightPlanRouteUtils = FlightPlanRouteUtils;
    exports.FlightPlanSegment = FlightPlanSegment;
    exports.FlightPlanUtils = FlightPlanUtils;
    exports.FlightPlanner = FlightPlanner;
    exports.FlightTimerController = FlightTimerController;
    exports.FlightTimerInstrument = FlightTimerInstrument;
    exports.FlightTimerPublisher = FlightTimerPublisher;
    exports.FlightTimerUtils = FlightTimerUtils;
    exports.FmcComponent = FmcComponent;
    exports.FmcListUtility = FmcListUtility;
    exports.FmcPageFactory = FmcPageFactory;
    exports.FmcRouter = FmcRouter;
    exports.FmcScratchpad = FmcScratchpad;
    exports.FmcScreen = FmcScreen;
    exports.Fragment = Fragment;
    exports.FsBaseInstrument = FsBaseInstrument;
    exports.FuelSystemSimVarPublisher = FuelSystemSimVarPublisher;
    exports.GNSSPublisher = GNSSPublisher;
    exports.GPSSatComputer = GPSSatComputer;
    exports.GPSSatellite = GPSSatellite;
    exports.GameStateProvider = GameStateProvider;
    exports.GameVarPublisher = GameVarPublisher;
    exports.GenericFlcComputer = GenericFlcComputer;
    exports.GenericMapSharedCanvasSubLayer = GenericMapSharedCanvasSubLayer;
    exports.GeoCircle = GeoCircle;
    exports.GeoCircleLineRenderer = GeoCircleLineRenderer;
    exports.GeoCirclePathRenderer = GeoCirclePathRenderer;
    exports.GeoCirclePatternRenderer = GeoCirclePatternRenderer;
    exports.GeoCircleResampler = GeoCircleResampler;
    exports.GeoKdTree = GeoKdTree;
    exports.GeoMath = GeoMath;
    exports.GeoPoint = GeoPoint;
    exports.GeoPointReadOnly = GeoPointReadOnly;
    exports.GeoPointSubject = GeoPointSubject;
    exports.GeoProjectionPathStream = GeoProjectionPathStream;
    exports.GeoProjectionPathStreamStack = GeoProjectionPathStreamStack;
    exports.GeodesicResampler = GeodesicResampler;
    exports.GetRegisteredSimVarValue = GetRegisteredSimVarValue;
    exports.GetSimVarStructType = GetSimVarStructType;
    exports.GlidePathCalculator = GlidePathCalculator;
    exports.GpsSynchronizer = GpsSynchronizer;
    exports.HEventPublisher = HEventPublisher;
    exports.HandlerSubscription = HandlerSubscription;
    exports.HardwareUiControl = HardwareUiControl;
    exports.HardwareUiControlList = HardwareUiControlList;
    exports.HeadingToAltitudeLegCalculator = HeadingToAltitudeLegCalculator;
    exports.HeadingToManualLegCalculator = HeadingToManualLegCalculator;
    exports.HoldLegCalculator = HoldLegCalculator;
    exports.HoldUtils = HoldUtils;
    exports.HorizonCanvasLayer = HorizonCanvasLayer;
    exports.HorizonCanvasLayerCanvasInstanceClass = HorizonCanvasLayerCanvasInstanceClass;
    exports.HorizonComponent = HorizonComponent;
    exports.HorizonLayer = HorizonLayer;
    exports.HorizonProjection = HorizonProjection;
    exports.HorizonSharedCanvasLayer = HorizonSharedCanvasLayer;
    exports.HorizonSharedCanvasSubLayer = HorizonSharedCanvasSubLayer;
    exports.HorizonSyncedCanvasLayer = HorizonSyncedCanvasLayer;
    exports.HydraulicsPublisher = HydraulicsPublisher;
    exports.ICAO = ICAO;
    exports.ImageCache = ImageCache;
    exports.InputAcceleration = InputAcceleration;
    exports.InstrumentBackplane = InstrumentBackplane;
    exports.InterceptGreatCircleToPointVectorBuilder = InterceptGreatCircleToPointVectorBuilder;
    exports.IntersectionFacilityUtils = IntersectionFacilityUtils;
    exports.IntervalPacer = IntervalPacer;
    exports.JetAutothrottle = JetAutothrottle;
    exports.JetFadec = JetFadec;
    exports.JoinGreatCircleToPointVectorBuilder = JoinGreatCircleToPointVectorBuilder;
    exports.KdTree = KdTree;
    exports.KeyEventManager = KeyEventManager;
    exports.LNavComputer = LNavComputer;
    exports.LNavObsManager = LNavObsManager;
    exports.LNavObsSimVarPublisher = LNavObsSimVarPublisher;
    exports.LNavRollSteerComputer = LNavRollSteerComputer;
    exports.LNavUtils = LNavUtils;
    exports.LatLonDisplay = LatLonDisplay;
    exports.LegInterceptLegCalculator = LegInterceptLegCalculator;
    exports.LerpLookupTable = LerpLookupTable;
    exports.LerpVectorLookupTable = LerpVectorLookupTable;
    exports.LinearServo = LinearServo;
    exports.LiquidDropPublisher = LiquidDropPublisher;
    exports.LodBoundary = LodBoundary;
    exports.LodBoundaryCache = LodBoundaryCache;
    exports.Lookahead = Lookahead;
    exports.MagVar = MagVar;
    exports.MapAbstractAirspaceRenderer = MapAbstractAirspaceRenderer;
    exports.MapAirspaceLayer = MapAirspaceLayer;
    exports.MapAirspaceModule = MapAirspaceModule;
    exports.MapAltitudeArcLayer = MapAltitudeArcLayer;
    exports.MapAltitudeArcModule = MapAltitudeArcModule;
    exports.MapAutopilotPropsModule = MapAutopilotPropsModule;
    exports.MapBindingsController = MapBindingsController;
    exports.MapBingLayer = MapBingLayer;
    exports.MapBlankWaypointIcon = MapBlankWaypointIcon;
    exports.MapCachedCanvasLayer = MapCachedCanvasLayer;
    exports.MapCachedCanvasLayerCanvasInstanceClass = MapCachedCanvasLayerCanvasInstanceClass;
    exports.MapCanvasLayer = MapCanvasLayer;
    exports.MapCanvasLayerCanvasInstanceClass = MapCanvasLayerCanvasInstanceClass;
    exports.MapClockUpdateController = MapClockUpdateController;
    exports.MapComponent = MapComponent;
    exports.MapCullableLocationTextLabel = MapCullableLocationTextLabel;
    exports.MapCullableTextLabelManager = MapCullableTextLabelManager;
    exports.MapCullableTextLayer = MapCullableTextLayer;
    exports.MapDataIntegrityModule = MapDataIntegrityModule;
    exports.MapFieldOfViewCalculator = MapFieldOfViewCalculator;
    exports.MapFlightPlanController = MapFlightPlanController;
    exports.MapFlightPlanModule = MapFlightPlanModule;
    exports.MapFollowAirplaneController = MapFollowAirplaneController;
    exports.MapFollowAirplaneModule = MapFollowAirplaneModule;
    exports.MapGenericLayer = MapGenericLayer;
    exports.MapIndexedRangeModule = MapIndexedRangeModule;
    exports.MapLabeledRingCanvasSubLayer = MapLabeledRingCanvasSubLayer;
    exports.MapLabeledRingLayer = MapLabeledRingLayer;
    exports.MapLayer = MapLayer;
    exports.MapLineLayer = MapLineLayer;
    exports.MapLocationTextLabel = MapLocationTextLabel;
    exports.MapModel = MapModel;
    exports.MapMultiLineAirspaceRenderer = MapMultiLineAirspaceRenderer;
    exports.MapNearestWaypointsLayer = MapNearestWaypointsLayer;
    exports.MapNearestWaypointsLayerSearch = MapNearestWaypointsLayerSearch;
    exports.MapOwnAirplaneIconModule = MapOwnAirplaneIconModule;
    exports.MapOwnAirplaneIconOrientationController = MapOwnAirplaneIconOrientationController;
    exports.MapOwnAirplaneLayer = MapOwnAirplaneLayer;
    exports.MapOwnAirplanePropsController = MapOwnAirplanePropsController;
    exports.MapOwnAirplanePropsModule = MapOwnAirplanePropsModule;
    exports.MapProjection = MapProjection;
    exports.MapRangeModule = MapRangeModule;
    exports.MapRotationController = MapRotationController;
    exports.MapRotationModule = MapRotationModule;
    exports.MapSharedCachedCanvasLayer = MapSharedCachedCanvasLayer;
    exports.MapSharedCachedCanvasSubLayer = MapSharedCachedCanvasSubLayer;
    exports.MapSharedCanvasLayer = MapSharedCanvasLayer;
    exports.MapSharedCanvasSubLayer = MapSharedCanvasSubLayer;
    exports.MapSingleLineAirspaceRenderer = MapSingleLineAirspaceRenderer;
    exports.MapSubject = MapSubject;
    exports.MapSyncedCanvasLayer = MapSyncedCanvasLayer;
    exports.MapSystemBuilder = MapSystemBuilder;
    exports.MapSystemComponent = MapSystemComponent;
    exports.MapSystemController = MapSystemController;
    exports.MapSystemFlightPlanLayer = MapSystemFlightPlanLayer;
    exports.MapSystemGenericController = MapSystemGenericController;
    exports.MapSystemIconFactory = MapSystemIconFactory;
    exports.MapSystemKeys = MapSystemKeys;
    exports.MapSystemLabelFactory = MapSystemLabelFactory;
    exports.MapSystemLegRenderer = MapSystemLegRenderer;
    exports.MapSystemPlanRenderer = MapSystemPlanRenderer;
    exports.MapSystemTrafficLayer = MapSystemTrafficLayer;
    exports.MapSystemUtils = MapSystemUtils;
    exports.MapSystemWaypointsLayer = MapSystemWaypointsLayer;
    exports.MapSystemWaypointsRenderer = MapSystemWaypointsRenderer;
    exports.MapTerrainColorsModule = MapTerrainColorsModule;
    exports.MapTrafficModule = MapTrafficModule;
    exports.MapWaypointDisplayModule = MapWaypointDisplayModule;
    exports.MapWaypointImageIcon = MapWaypointImageIcon;
    exports.MapWaypointRenderer = MapWaypointRenderer;
    exports.MapWaypointRendererEntry = MapWaypointRendererEntry;
    exports.MapWaypointSpriteIcon = MapWaypointSpriteIcon;
    exports.MapWxrModule = MapWxrModule;
    exports.MappedSubject = MappedSubject;
    exports.MappedUserSettingManager = MappedUserSettingManager;
    exports.MappedValue = MappedValue;
    exports.MathUtils = MathUtils;
    exports.MercatorProjection = MercatorProjection;
    exports.MinimumsManager = MinimumsManager;
    exports.MinimumsSimVarPublisher = MinimumsSimVarPublisher;
    exports.MultiExpSmoother = MultiExpSmoother;
    exports.NavComConfig = NavComConfig;
    exports.NavComInstrument = NavComInstrument;
    exports.NavComSimVarPublisher = NavComSimVarPublisher;
    exports.NavMath = NavMath;
    exports.NavProcSimVarPublisher = NavProcSimVarPublisher;
    exports.NavProcessor = NavProcessor;
    exports.NavProcessorConfig = NavProcessorConfig;
    exports.NearestAirportSearchSession = NearestAirportSearchSession;
    exports.NearestAirportSubscription = NearestAirportSubscription;
    exports.NearestBoundarySearchSession = NearestBoundarySearchSession;
    exports.NearestContext = NearestContext;
    exports.NearestIntersectionSearchSession = NearestIntersectionSearchSession;
    exports.NearestIntersectionSubscription = NearestIntersectionSubscription;
    exports.NearestLodBoundarySearchSession = NearestLodBoundarySearchSession;
    exports.NearestNdbSubscription = NearestNdbSubscription;
    exports.NearestRepoFacilitySearchSession = NearestRepoFacilitySearchSession;
    exports.NearestUsrSubscription = NearestUsrSubscription;
    exports.NearestVorSearchSession = NearestVorSearchSession;
    exports.NearestVorSubscription = NearestVorSubscription;
    exports.NoPathLegCalculator = NoPathLegCalculator;
    exports.NodeReference = NodeReference;
    exports.NullAirspaceRenderer = NullAirspaceRenderer;
    exports.NullPathStream = NullPathStream;
    exports.NumberFormatter = NumberFormatter;
    exports.NumberUnit = NumberUnit;
    exports.NumberUnitReadOnly = NumberUnitReadOnly;
    exports.NumberUnitSubject = NumberUnitSubject;
    exports.ObjectSubject = ObjectSubject;
    exports.OrthographicProjection = OrthographicProjection;
    exports.PageConstructorUtils = PageConstructorUtils;
    exports.PageLinkField = PageLinkField;
    exports.PassThroughPathStream = PassThroughPathStream;
    exports.PatternPathStream = PatternPathStream;
    exports.PerformancePlanRepository = PerformancePlanRepository;
    exports.PerformancePlanUtils = PerformancePlanUtils;
    exports.PidController = PidController;
    exports.PitotPublisher = PitotPublisher;
    exports.PlanSubjects = PlanSubjects;
    exports.PluginSystem = PluginSystem;
    exports.PressurizationPublisher = PressurizationPublisher;
    exports.ProcedureTurnLegCalculator = ProcedureTurnLegCalculator;
    exports.ProcedureTurnVectorBuilder = ProcedureTurnVectorBuilder;
    exports.ProxiedPerformancePlanProperty = ProxiedPerformancePlanProperty;
    exports.RadialInterceptLegCalculator = RadialInterceptLegCalculator;
    exports.RadioFrequencyFormatter = RadioFrequencyFormatter;
    exports.RadioUtils = RadioUtils;
    exports.RadiusToFixLegCalculator = RadiusToFixLegCalculator;
    exports.RandomNumberPublisher = RandomNumberPublisher;
    exports.RawFormatter = RawFormatter;
    exports.RawValidator = RawValidator;
    exports.ResolveRegisteredSimVar = ResolveRegisteredSimVar;
    exports.ResourceHeap = ResourceHeap;
    exports.ResourceModerator = ResourceModerator;
    exports.RunwayUtils = RunwayUtils;
    exports.SVGUtils = SVGUtils;
    exports.ScrollUtils = ScrollUtils;
    exports.SetSubject = SetSubject;
    exports.SimOvrd = SimOvrd;
    exports.SimVarPublisher = SimVarPublisher;
    exports.SimpleFmcRenderer = SimpleFmcRenderer;
    exports.SimpleMovingAverage = SimpleMovingAverage;
    exports.SimpleUnit = SimpleUnit;
    exports.SmoothingPathCalculator = SmoothingPathCalculator;
    exports.SortedArray = SortedArray;
    exports.SortedMappedSubscribableArray = SortedMappedSubscribableArray;
    exports.SoundServer = SoundServer;
    exports.SoundServerController = SoundServerController;
    exports.StallWarningPublisher = StallWarningPublisher;
    exports.StringUtils = StringUtils;
    exports.SubEvent = SubEvent;
    exports.Subject = Subject;
    exports.SubscribableMapFunctions = SubscribableMapFunctions;
    exports.SubscribableMapPipe = SubscribableMapPipe;
    exports.SubscribablePipe = SubscribablePipe;
    exports.SubscribableSetPipe = SubscribableSetPipe;
    exports.SubscribableUtils = SubscribableUtils;
    exports.SvgPathStream = SvgPathStream;
    exports.SwitchLabel = SwitchLabel;
    exports.SynVisComponent = SynVisComponent;
    exports.SystemAlertManager = SystemAlertManager;
    exports.SystemAlertPublisher = SystemAlertPublisher;
    exports.Table3D = Table3D;
    exports.TacanUtils = TacanUtils;
    exports.Tcas = Tcas;
    exports.TcasIISensitivity = TcasIISensitivity;
    exports.TcasIISensitivityParameters = TcasIISensitivityParameters;
    exports.TemperatureSystem = TemperatureSystem;
    exports.TextInputField = TextInputField;
    exports.ThrottleLeverManager = ThrottleLeverManager;
    exports.ThrottledTaskQueueProcess = ThrottledTaskQueueProcess;
    exports.ToggleLabel = ToggleLabel;
    exports.TrackFromFixLegCalculator = TrackFromFixLegCalculator;
    exports.TrackToFixLegCalculator = TrackToFixLegCalculator;
    exports.TrafficInstrument = TrafficInstrument;
    exports.Transform2D = Transform2D;
    exports.Transform3D = Transform3D;
    exports.TransformPerspective = TransformPerspective;
    exports.TransformingPathStreamStack = TransformingPathStreamStack;
    exports.TurbopropAutothrottle = TurbopropAutothrottle;
    exports.TurbopropFadec = TurbopropFadec;
    exports.TurnToFixLegCalculator = TurnToFixLegCalculator;
    exports.TurnToJoinGreatCircleAtPointVectorBuilder = TurnToJoinGreatCircleAtPointVectorBuilder;
    exports.TurnToJoinGreatCircleVectorBuilder = TurnToJoinGreatCircleVectorBuilder;
    exports.UUID = UUID;
    exports.UnitType = UnitType;
    exports.UserFacilityUtils = UserFacilityUtils;
    exports.UserSettingSaveManager = UserSettingSaveManager;
    exports.VNavControlEventPublisher = VNavControlEventPublisher;
    exports.VNavDataEventPublisher = VNavDataEventPublisher;
    exports.VNavSimVarPublisher = VNavSimVarPublisher;
    exports.VNavUtils = VNavUtils;
    exports.VNavWaypoint = VNavWaypoint;
    exports.Value = Value;
    exports.Vec2Math = Vec2Math;
    exports.Vec2Subject = Vec2Subject;
    exports.Vec3Math = Vec3Math;
    exports.Vec3Subject = Vec3Subject;
    exports.VecNMath = VecNMath;
    exports.VecNSubject = VecNSubject;
    exports.Wait = Wait;
    exports.Warning = Warning;
    exports.WarningManager = WarningManager;
    exports.WaypointDisplayBuilder = WaypointDisplayBuilder;
    exports.WeightBalanceSimvarPublisher = WeightBalanceSimvarPublisher;
    exports.XMLAnnunciationFactory = XMLAnnunciationFactory;
    exports.XMLGaugeConfigFactory = XMLGaugeConfigFactory;
    exports.XMLWarningFactory = XMLWarningFactory;
    exports.XPDRInstrument = XPDRInstrument;
    exports.XPDRSimVarPublisher = XPDRSimVarPublisher;
    exports.registerPlugin = registerPlugin;

    return exports;

})({});
//# sourceMappingURL=msfssdk.js.map
