| Scripts/route-planner-interface.js | "use strict";
this.name = "RoutePlanner_Interface";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
this.$ILib = null;
this.$screens = {};
this.$inverseMapTo = function (map) {
    return Object.keys(map).reduce(function (invMap, key) {
        return invMap[map[key]] = key;
    }.bind(this), {})
}
this.$findKeyByValue = function (dictionary, value) {
    var keys = Object.keys(dictionary);
    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        if (dictionary[key] === value)
            return key;
    }
    return undefined;
}
this.$colorToColorSpecifierMap = {
    "Green": "greenColor",
    "Yellow": "yellowColor",
    "Red": "redColor",
    "Blue": "blueColor",
    "Cyan": "cyanColor",
}
this.$colorSpecifierToColorMap = this.$inverseMapTo(this.$colorToColorSpecifierMap)
this.$markerShapes = {
    "Cross": "MARKER_X",
    "Plus": "MARKER_PLUS",
    "Square": "MARKER_SQUARE",
    "Diamond": "MARKER_DIAMOND"
}
this.$configureInterface = function () {
    let interfaceScript = this;
    let mainScript = worldScripts["RoutePlanner"];
    let ILib = this.$ILib = worldScripts["RoutePlanner_InterfaceLib"];
    mainScreen: {
        const mainScreen = this.$screens.main = new ILib.PagedScreenController({
            title: "Route Planning Interface",
            screenID: "RoutePlanner_Main",
            allowInterrupt: true,
            exitScreen: "GUI_SCREEN_INTERFACES"
        });
        mainScreen.addToContent(new ILib.LabelLine("General"));
        mainScreen.addToContent(
            new ILib.CycleLine(
                expandDescription("[RoutePlanner_interface_main_wp-order-mode]"),
                mainScript.$waypointOrderModes,
                mainScript.$currentWaypointOrderMode)
                .onSelected(function (selectedIndex, selectedValue) {
                    mainScript.$currentWaypointOrderMode = selectedValue;
                })
        );
        mainScreen.addToContent(
            new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_edit-route]"))
                .onUpdate(function () {
                    return {
                        unselectable: mainScript.$currentWaypointOrderMode === "None"
                    };
                })
                .onSelected(function () {
                    this.$screens.editRouteInstructions.runScreen(mainScript.$currentWaypointOrderMode)
                    return true; // returning true prevent current screen redrawing
                }.bind(this))
        );
        mainScreen.addToContent(
            new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_route-statistics]"))
                .onUpdate(function () {
                    return {
                        unselectable: mainScript.$currentWaypointOrderMode === "None"
                    };
                })
                .onSelected(function () {
                    this.$screens.routeStatisticsScreen.runScreen()
                    return true;
                }.bind(this))
        );
        const wpTypes = mainScript.$wpTypes;
        const wpTypesList = Object.keys(wpTypes);
        for (let i = 0; i < wpTypesList.length; i++)
        {
            let typeKey = wpTypesList[i];
            let type = wpTypes[typeKey];
            let labelText = expandDescription(
                "[RoutePlanner_interface_main_route-through-toggler]",
                {waypointType: expandDescription(type.nameKey)});
            mainScreen.addToContent(
                new ILib.ToggleLine(labelText, type.isUsed)
                    .onSelected(function (newState) {
                        type.isUsed = newState;
                    })
            )   
        }        
        mainScreen.addToContent(
            new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_appearance-settings]"))
                .onSelected(function () {
                    this.$screens.appearanceSettings.runScreen()
                    return true; // returning true prevent current screen redrawing
                }.bind(this))
        );
    }
    const returnToMain = this.$screens.main.returnToScreen.bind(this.$screens.main);
    appearanceSettingsScreen: {
        const appearanceSettingsScreen = this.$screens.appearanceSettings = new ILib.PagedScreenController({
            title: "Route Planning Interface: Appearance Settings",
            screenID: "RoutePlanner_Main_AppearanceSettings",
            allowInterrupt: true,
            exitScreen: "GUI_SCREEN_INTERFACES"
        }, returnToMain);
        const colors = this.$colorToColorSpecifierMap;
        const appearance = mainScript.$appearance;
        appearanceSettingsScreen.addToContent(
            new ILib.CycleLine(
                expandDescription("[RoutePlanner_interface_appearance_user-wp-marker-shape]"),
                this.$markerShapes,
                this.$findKeyByValue(this.$markerShapes, appearance.userWaypointShape))
                .onSelected(function (selectedShapeIndex, selectedShape) {
                    appearance.userWaypointShape = selectedShape;
                })
        );
        appearanceSettingsScreen.addToContent(
            new ILib.CycleLine(
                expandDescription("[RoutePlanner_interface_appearance_user-wp-marker-color]"),
                colors,
                this.$findKeyByValue(colors, appearance.userWaypointColor))
                .onSelected(function (selectedColorIndex, selectedColor) {
                    appearance.userWaypointColor = selectedColor;
                })
        );
        appearanceSettingsScreen.addToContent(
            new ILib.CycleLine(
                expandDescription("[RoutePlanner_interface_appearance_user-route-color-jumps]"),
                colors,
                this.$findKeyByValue(colors, appearance.routeLineColorByJumps))
                .onSelected(function (selectedColorIndex, selectedColor) {
                    appearance.routeLineColorByJumps = selectedColor;
                })
        );
        appearanceSettingsScreen.addToContent(
            new ILib.CycleLine(
                expandDescription("[RoutePlanner_interface_appearance_user-route-color-time]"),
                colors,
                this.$findKeyByValue(colors, appearance.routeLineColorByTime))
                .onSelected(function (selectedColorIndex, selectedColor) {
                    appearance.routeLineColorByTime = selectedColor;
                })
        );
    }
    function formatWaypoints(waypoints, highlightedSystemId, waypointOrderMode) {
        function getSystemSubstitution(id) {
            if (id < 10) return "%J00" + id;
            if (id < 100) return "%J0" + id;
            else return "%J" + id;
        }
        const wpSubtitutions = waypoints.map(function (wp) {
            if (highlightedSystemId === wp)
                return " \[" + getSystemSubstitution(wp) + "\] ";
            else
                return getSystemSubstitution(wp);
        });
        const wpSeparator = waypointOrderMode === "Auto" ? ", " : " → ";
        return wpSubtitutions.length? expandDescription(wpSubtitutions.join(wpSeparator)) : "None";
    }
    routeEditingInstruction: {
        const instructionSP = {
            title: "Route Planning Interface: Route editing instruction",
            screenID: "RoutePlanner_EditRoute_Instructions",
            allowInterrupt: false,
            exitScreen: "GUI_SCREEN_SHORT_RANGE_CHART",
        }
        instructionSP.choices = {
            "00_SPACER" : { text: "" },
            "01_RETURN": {
                text: ILib.$padTextOnSide(
                    expandDescription("[RoutePlanner_interface_route-editing-instructions_return]"), 
                    "right", 32),
                color: "cyanColor"
            },
            "02_ADD_START": {
                text: ILib.$padTextOnSide(
                    expandDescription("[RoutePlanner_interface_route-editing-instructions_add-start]"), 
                    "right", 32),
                color: "cyanColor"
            },
            "03_CONTINUE": {
                text: ILib.$padTextOnSide(
                    expandDescription("[RoutePlanner_interface_route-editing-instructions_start-edit-on-chart]"), 
                    "right", 32),
                color: "cyanColor"
            },
            "04_SPACER": { text: "" },
            "05_SPACER": { text: "" },
        }
        this.$screens.editRouteInstructions = {
            screenParameters: instructionSP,
            returnToMain: returnToMain,
            userWaypoints: mainScript.$userWaypoints,
            runScreen: function (waypointOrderMode) {
                let targetSystem = player.ship.targetSystem;
                let firstUserWaypoint = this.userWaypoints.length === 0? -1 : this.userWaypoints[0];
                const addToStartChoise = this.screenParameters.choices["02_ADD_START"];
                if(system.ID === targetSystem || firstUserWaypoint === targetSystem)
                {
                    addToStartChoise.unselectable = true;
                    addToStartChoise.color = "darkGrayColor";
                }
                else
                {
                    addToStartChoise.unselectable = false;
                    addToStartChoise.color = "cyanColor";
                }
                let callback = function (choice) {
                    if (choice === "01_RETURN") this.returnToMain();
                    else if (choice === "02_ADD_START") 
                    {                                            
                        this.userWaypoints.unshift(targetSystem);
                        interfaceScript.$screens.editRouteInstructions.runScreen(waypointOrderMode);
                    }
                    else mainScript.$routeEditingEnabled = true;
                };
                
                
                instructionSP.message = expandMissionText("RoutePlanner_interface_route-editing-instructions", {
                    waypointOrderMode: waypointOrderMode,
                    userWaypoints: formatWaypoints(this.userWaypoints, targetSystem, waypointOrderMode)
                });
                mission.runScreen(this.screenParameters, callback, this);
            },
        }
    }
    routeEditingActions: {
        const routeEditActionSP = {
            title: "Route Planning Interface: <systemname>",
            screenID: "RoutePlanner_EditRoute_SystemAction",
            allowInterrupt: false,
            exitScreen: "GUI_SCREEN_SHORT_RANGE_CHART",
            choices: {}
        }
        this.$screens.editRouteSystemAction = {
            screenParameters: routeEditActionSP,
            returnToMain: returnToMain,
            userWaypoints: mainScript.$userWaypoints,
            selectedSystemId: null,
            selectedSystemIndex: null,
            systemId: null,
            systemIndex: null,
            runScreen: function (systemId, waypointOrderMode) {
                const systemIndex = this.userWaypoints.indexOf(systemId);
                const selectedSystemId = this.selectedSystemId;
                this.systemId = systemId;
                this.systemIndex = systemIndex;
                const systemInWaypoints = systemIndex !== -1;
                const systemName = System.systemNameForID(systemId);
                let selectedSystemName = undefined;
                let systemIsSelected = false;
                if (selectedSystemId === null)
                    this.selectedSystemIndex = null;
                else {
                    this.selectedSystemIndex = this.userWaypoints.indexOf(selectedSystemId);
                    selectedSystemName = System.systemNameForID(selectedSystemId);
                    systemIsSelected = systemId === selectedSystemId;
                }
                this.screenParameters.message = this.getText(systemId, waypointOrderMode);
                this.screenParameters.choices = this.getChoices(systemInWaypoints, systemIsSelected, selectedSystemName);
                this.screenParameters.title = "Route Planning Interface: " + systemName;
                mission.runScreen(this.screenParameters, this.screenCallback, this);
            },
            getText: function (highlightedSystemId, waypointOrderMode) {
                
                return expandMissionText("RoutePlanner_interface_route-editing-actions", {
                    waypointOrderMode: waypointOrderMode,
                    userWaypoints: formatWaypoints(this.userWaypoints, highlightedSystemId, waypointOrderMode)
                });
            },
            getChoices: function (systemInWaypoints, systemIsSelected, selectedSystemName) {
                const choices = {};
                if (systemInWaypoints) {
                    if (systemIsSelected)
                        choices["01_DESELECT"] = "Deselect";
                    else
                        choices["01_SELECT"] = "Select";
                    choices["02_REMOVE"] = "Remove";
                    if (selectedSystemName) {
                        choices["03_SWAP"] = "Swap with " + selectedSystemName;
                    }
                    // choices["03_SPACER"] = "";
                }
                else {
                    if (selectedSystemName) {
                        choices["01_ADD_BEFORE"] = "Add before " + selectedSystemName;
                        choices["02_ADD_AFTER"] = "Add after " + selectedSystemName;
                    }
                    else {
                        choices["01_SPACER"] = "";
                        choices["02_SPACER"] = "";
                    }
                    choices["03_ADD_END"] = "Add to the end";
                }
                choices["04_SPACER"] = ""
                choices["05_BACK_TO_CHART"] = "Back to chart";
                choices["06_STOP_EDIT"] = "Stop route editing";
                return choices;
            },
            screenCallback: function (choice) {
                if (choice === "05_BACK_TO_CHART") return;
                else if (choice === "06_STOP_EDIT") {
                    mainScript.$routeEditingEnabled = false;
                    this.returnToMain();
                    return;
                }
                if (this.systemIndex !== -1) {
                    if (choice === "01_SELECT")
                        this.selectedSystemId = this.systemId;
                    else if (choice === "01_DESELECT")
                        this.selectedSystemId = null;
                    else if (choice === "02_REMOVE") {
                        this.userWaypoints.splice(this.systemIndex, 1);
                        if (this.selectedSystemIndex === this.systemIndex)
                            this.selectedSystemId = null
                    }
                    else if (choice === "03_SWAP") {
                        let temp = this.userWaypoints[this.systemIndex];
                        this.userWaypoints[this.systemIndex] = this.userWaypoints[this.selectedSystemIndex];
                        this.userWaypoints[this.selectedSystemIndex] = temp;
                        this.selectedSystemIndex = this.systemIndex;
                    }
                }
                else {
                    if (choice == "01_ADD_BEFORE")
                        this.userWaypoints.splice(this.selectedSystemIndex, 0, this.systemId)
                    else if (choice == "02_ADD_AFTER")
                        this.userWaypoints.splice(this.selectedSystemIndex + 1, 0, this.systemId)
                    else if (choice == "03_ADD_END")
                        this.userWaypoints.push(this.systemId)
                }
            }
        }
    }
    routeStatistics: {
        const routeStatisticsScreen = this.$screens.routeStatisticsScreen = new ILib.PagedScreenController({
            title: "Route Planning Interface: Statistics",
            screenID: "RoutePlanner_Main_RouteStatistics",
            allowInterrupt: true,
            exitScreen: "GUI_SCREEN_INTERFACES"
        }, returnToMain);
        let formatToColumns = function (name, type, remainTime, travelTime, distance){
            return "" +
                ILib.$padTextOnSide(name, "right", 6) + 
                ILib.$padTextAround(type, 8) +
                ILib.$padTextOnSide(remainTime, "left", 6) +
                ILib.$padTextOnSide(travelTime, "left", 6) +
                ILib.$padTextOnSide(distance, "left", 6);
        } 
        
        routeStatisticsScreen.addToHeader(
            new ILib.LabelLine(formatToColumns("Name", "Type", "Remain time", "Travel time", "Distance, LY"))
        );
        routeStatisticsScreen.addToHeader(new ILib.LabelLine(ILib.$repeatPattern("-", 32)));        
        
        let rsScreenRun = routeStatisticsScreen.runScreen.bind(routeStatisticsScreen);
        let updateTable = function (){
            const playerShip = player.ship;
            const targetSystem = playerShip.targetSystem;
            const memoizedRoutes = mainScript.$memoizedRoutes;
            const routeMode = playerShip.routeMode;
            const distanceMeasurement = routeMode === "OPTIMIZED_BY_JUMPS" ? "distance" : "distanceWhenTime";
            const timeMeasurement     = routeMode === "OPTIMIZED_BY_JUMPS" ? "timeWhenDistance" : "time";
            const waypointsOnCurrentRoute = mainScript.$waypointsOnCurrentRoute;
            const waypointsOnCurrentRouteCount = waypointsOnCurrentRoute.length;
            const storedRoutes = mainScript.$storedRoutes;
            const storedRoutesCount = storedRoutes.length;
            
            routeStatisticsScreen.clearContent();
            let totalTime = 0;
            let totalDistance = 0;
            let previousWaypointId = system.ID;   
            for(let i = waypointsOnCurrentRouteCount - 1; i >= 0; i--)
            {
                let waypointInfo = waypointsOnCurrentRoute[i];
                if(waypointInfo === undefined) // if something went wrong then just ignore it and hope for better
                    continue;
                let waypointId = waypointInfo.ID;
                let name = System.systemNameForID(waypointId);
                let costs = memoizedRoutes.routeCost(previousWaypointId, waypointId);
                totalDistance += costs[distanceMeasurement];
                totalTime += costs[timeMeasurement];
                let type = "", remainTime = "";
                if(waypointInfo.type)
                    type = expandDescription(waypointInfo.type.nameKey);
                if(waypointId === targetSystem)
                    type += ", " + expandDescription("[RoutePlanner_wpType_targetSystem]");
                if(waypointInfo.eta)
                    remainTime = ((waypointInfo.eta - clock.seconds) / 3600).toFixed(1);
                routeStatisticsScreen.addToContent(
                    new ILib.LabelLine(formatToColumns(name, type, remainTime, totalTime.toFixed(1), totalDistance.toFixed(1)))
                );
                previousWaypointId = waypointId;
            }   
            if(targetSystem !== previousWaypointId) {
                let name = System.systemNameForID(targetSystem);
                let costs = memoizedRoutes.routeCost(system.ID, targetSystem);
                totalDistance = costs[distanceMeasurement];
                totalTime = costs[timeMeasurement];
                routeStatisticsScreen.addToContent(
                    new ILib.LabelLine(formatToColumns(
                        name, expandDescription("[RoutePlanner_wpType_targetSystem]"), 
                        "", totalTime.toFixed(1), totalDistance.toFixed(1)))
                );
                previousWaypointId = targetSystem;
            }
            for(let i = 0; i < storedRoutesCount; i++)
            {
                let waypointInfo = storedRoutes[i].endWaypointInfo;
                if(waypointInfo === undefined) // if something went wrong then just ignore it and hope for better 2
                    continue;
                let waypointId = waypointInfo.ID;
                let name = System.systemNameForID(waypointId);
                let costs = memoizedRoutes.routeCost(previousWaypointId, waypointId);
                totalDistance += costs[distanceMeasurement];
                totalTime += costs[timeMeasurement];
                let type = "", remainTime = "";
                if(waypointInfo.type)
                    type = expandDescription(waypointInfo.type.nameKey);
                if(waypointInfo.eta)
                    remainTime = ((waypointInfo.eta - clock.seconds) / 3600).toFixed(1);
                routeStatisticsScreen.addToContent(
                    new ILib.LabelLine(formatToColumns(name, type, remainTime, totalTime.toFixed(1), totalDistance.toFixed(1)))
                );
                previousWaypointId = waypointId;
            }
            
            routeStatisticsScreen.clearFooter();
            routeStatisticsScreen.addToFooter(new ILib.LabelLine(ILib.$repeatPattern("-", 32)));  
            routeStatisticsScreen.addToFooter(
                new ILib.LabelLine(formatToColumns("Total", "", "", totalTime.toFixed(1), totalDistance.toFixed(1)))
            );
        }
        
        routeStatisticsScreen.runScreen = function() { updateTable(); rsScreenRun(); }
    }
}
 | 
                
                    | Scripts/route-planner-interfaceLib.js | "use strict";
this.name = "RoutePlanner_InterfaceLib";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
(function() {
    let ILib = this;
    this.__BasePagedController = function __BasePagedController(screenParameters) {
        __BasePagedController_init(screenParameters);
    }
    this.__BasePagedController.prototype.__BasePagedController_init = function (screenParameters)  {
        this.screenParameters = screenParameters;
        this.headerLines = [];
        this.contentLines = [];
        this.footerLines = [];
        this.headerPrefix = "00HEADER";
        this.contentPrefix = "20CONTENT";
        this.footerPrefix = "40FOOTER";
        this.currentPage = 0;
        this.pages = 0;
        this.selectedChoicesKey = undefined;
    }
    this.__BasePagedController.prototype.addToHeader = function addToHeader(line) {
        const headerLinesCount = this.headerLines.length;
        const key = (headerLinesCount < 10 ? ("0" + headerLinesCount) : headerLinesCount) + "_NO_KEY";
        this.headerLines.push({ key: key, line: line });
    }
    this.__BasePagedController.prototype.addToContent = function addToContent(line) {
        const contentLinesCount = this.contentLines.length;
        const key = (contentLinesCount < 10 ? ("0" + contentLinesCount) : contentLinesCount) + "_NO_KEY";
        this.contentLines.push({ key: key, line: line });
    }
    this.__BasePagedController.prototype.addToFooter = function addToFooter(line) {
        const footerLinesCount = this.footerLines.length;
        const key = (footerLinesCount < 10 ? ("0" + footerLinesCount) : footerLinesCount) + "_NO_KEY";
        this.footerLines.push({ key: key, line: line });
    }
    /*
        Remove line from ...
        this.__BasePagedScreenController.prototype.removeLineFromContent = function removeFromContent(line) {
        let removePos = -1;
        let i = this.contentLines.length;
        while (i--) {
            if (this.contentLines[i].line === line) {
                removePos = i;
                break;
            }
        }
        if (removePos === -1)
            return false;
        else {
            this.contentLines.splice(removePos, 1);
            return true;
        }
    }
    */
    this.__BasePagedController.prototype.clearHeader = function clearHeader() {
        this.headerLines.length = 0;
    }
    this.__BasePagedController.prototype.clearContent = function clearHeader() {
        this.contentLines.length = 0;
    }
    this.__BasePagedController.prototype.clearFooter = function clearHeader() {
        this.footerLines.length = 0;
    }
    /***
     * Update every line in header and fill "choises" parameter with them  
     * @param {object} choises
     * @returns {number} Header height in lines
     */
    this.__BasePagedController.prototype.updateHeader = function updateHeader(choises) {
        const linesForHeader = this.headerLines.length;
        for (let i = 0; i < linesForHeader; i++) {
            let pair = this.headerLines[i];
            let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesForHeader;
    }
    /**
     * Update content lines on specified page and fill "choises" parameter with them
     * @param {object} choises
     * @param {number} linesPerPage Maximum number of lines per page
     * @param {number} page Page number
     * @returns {number} Content height on specified page in lines
     */
    this.__BasePagedController.prototype.updateContent = function updateContent(choises, linesPerPage, page) {
        let linesStartIndex = linesPerPage * page;
        let linesEndIndex = linesStartIndex + linesPerPage;
        let contentLinesCount = this.contentLines.length;
        let linesEndIndexLimited = contentLinesCount < linesEndIndex ? contentLinesCount : linesEndIndex;
        for (let i = linesStartIndex; i < linesEndIndexLimited; i++) {
            let pair = this.contentLines[i];
            let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesEndIndexLimited - linesStartIndex;
    }
    /***
     * Update every line in footer and fill "choises" parameter with them  
     * @param {object} choises
     * @returns {number} Footer height in lines
     */
    this.__BasePagedController.prototype.updateFooter = function updateFooter(choises) {
        const linesForFooter = this.footerLines.length;
        
        for (let i = 0; i < linesForFooter; i++) {
            let pair = this.footerLines[i];
            let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesForFooter;
    }
    this.__BasePagedController.prototype.proccessChoises = function proccessChoises(choise) {
        for (let i = 0; i < this.headerLines.length; i++) {
            let pair = this.headerLines[i];
            let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select(); // if true returned then don`t redraw this screen
            }
        }
        
        for (let i = 0; i < this.contentLines.length; i++) {
            let pair = this.contentLines[i];
            let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select();
            }
        }
        for (let i = 0; i < this.footerLines.length; i++) {
            let pair = this.footerLines[i];
            let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select();
            }
        }
        // Restarting the screen, if necessary, is performed by the child 
        /* 
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = initialChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
        */
    }
    this.__BasePagedController.prototype.runScreen = function runScreen() {
        log("ROUTEPLANER_INTERFACELIB_DEBUG", "Started " + this.screenParameters.title + " mission screen");
        this.currentPage = 0;
        this.screenParameters.choices = this.getPage(0);
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.__BasePagedController.prototype.returnToScreen = function returnToScreen() {
        if(this.currentPage === undefined || this.currentPage > this.pages)
            this.currentPage = 0;
        log("ROUTEPLANER_INTERFACELIB_DEBUG", "Returned to " + this.screenParameters.title + " mission screen on page " + this.currentPage);
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    /**
     * Create paged screen that can be filled with line objects.
     * Each time the line is selected, it will re-render, 
     * unless the Return option is selected (then will be called callbackOnReturn)
     * or the onSelect callback of the selected line returns true
     * @param {*} screenParameters 
     * @param {Function} [callbackOnReturn]
     */
    this.PagedScreenController = function PagedScreenController(screenParameters, callbackOnReturn) {
        this.__BasePagedController_init(screenParameters); // Don`t know how to get base constructor properly
        this.callbackOnReturn = callbackOnReturn ?  callbackOnReturn : null;
    }
    this.PagedScreenController.prototype = Object.create(__BasePagedController.prototype);
    this.PagedScreenController.prototype.onReturn = function onReturn(callback) {
        this.callbackOnReturn = callback;
    }
    this.PagedScreenController.prototype.getPage = function getPage(page) {
        const screenHeight = (player.ship.hudAllowsBigGui ? 27 : 21);
        const choises = {};
        const headerHeight = this.updateHeader(choises);
        const footerHeight = this.updateFooter(choises);
        const controlsHeight = 5;
        const maxContentHeight = screenHeight - headerHeight - footerHeight - controlsHeight;
        const pages = this.pages = Math.ceil(this.contentLines.length / maxContentHeight);
        const contentHeight = this.updateContent(choises, maxContentHeight, page);
        const remainHeight = maxContentHeight - contentHeight;
        log("ROUTEPLANER_INTERFACELIB_DEBUG", "Remain lines after content " + remainHeight + " from " + maxContentHeight)
        /*** Fill empty ***/
        const spacer = { text: "" };
        for (let i = 0; i < remainHeight; i++) {
            let key = "21EMPTY_" + (i < 10 ? ("0" + i) : i) + "_EMPTY";
            choises[key] = spacer;
        }
        /*** Controlls ***/
        if (pages > 1) {
            choises["70_SPACER"] = spacer;
            choises["71_PAGE_NUM"] = {
                text: "Page " + (1 + page) + " of " + pages,
                unselectable: true
            }
            choises["73_NEXT_PAGE"] = {
                text: "Next page",
                unselectable: page + 1 === pages
            };
            choises["74_PREV_PAGE"] = {
                text: "Previous page",
                unselectable: page === 0
            };
        } else {
            choises["70_SPACER"] = spacer;
            choises["71_SPACER"] = spacer;
            choises["73_SPACER"] = spacer;
            choises["74_SPACER"] = spacer;
        }
        choises["80_RETURN"] = { text: "Return" };
        return choises;
    }
    this.PagedScreenController.prototype.onChoiceSelected = function onChoiceSelected(choise) {
        if (choise === "80_RETURN") {
            if (this.callbackOnReturn) this.callbackOnReturn();
            return;
        }
        this.selectedChoicesKey = choise;
        if (choise === "74_PREV_PAGE") {
            this.currentPage--;
            if (this.currentPage === 0)
                this.selectedChoicesKey = "73_NEXT_PAGE";
        }
        else if (choise === "73_NEXT_PAGE") {
            this.currentPage++;
            if (this.currentPage + 1 === this.pages)
                this.selectedChoicesKey = "74_PREV_PAGE";
        }
        else {
            const stopRestartingScreen = this.proccessChoises(choise);
            
            if(stopRestartingScreen)
            {
                log("ROUTEPLANER_INTERFACELIB_DEBUG", "Screen \"" + this.screenParameters.title + "\" restarting has been stopped by key " + choise)
                return;
            }
                
        }
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    
    this.$limitText = function $limitText(text, limitWidth) {
        const ellipsis = "…";
        let tmp = text;
        while (defaultFont.measureString(tmp) > limitWidth) {
            tmp = tmp.substring(0, tmp.length - 2) + ellipsis;
        }
        return tmp;
    }
    /**
     * 
     * @param {string} text 
     * @param {"left"|"right"} padSide 
     * @param {number} desiredWidth 
     * @returns Padded text 
     */
    this.$padTextOnSide = function $padTextOnSide(text, padSide, desiredWidth) {
        const hairSpace = String.fromCharCode(31);
        const hairSpaceWidth = defaultFont.measureString(hairSpace);
        const textWidth = defaultFont.measureString(text);
        const padsNeeded = Math.floor((desiredWidth - textWidth) / hairSpaceWidth);
        let resultText;
        if (padsNeeded > 1)
            if (padSide == "left")
                resultText = new Array(padsNeeded).join(hairSpace) + text;
            else
                resultText = text + new Array(padsNeeded).join(hairSpace);
        else
            resultText = this.$limitText(text, desiredWidth);
        return resultText;
    }
    this.$padTextAround = function $padTextAround(text, desiredWidth) {
        const hairSpace = String.fromCharCode(31);
        const hairSpaceWidth = defaultFont.measureString(hairSpace);
        const textWidth = defaultFont.measureString(text);
        const padsNeeded = Math.floor((desiredWidth - textWidth) / (2 * hairSpaceWidth));
        let resultText;
        if (padsNeeded > 1) {
            let sidePadding = new Array(padsNeeded).join(hairSpace);
            resultText = sidePadding + text + sidePadding;
        }
        else
            resultText = this.$limitText(text, desiredWidth);
        return resultText;
    }
    this.$repeatPattern = function $repeatPattern(pattern, desiredWidth) {
        const patternWidth = defaultFont.measureString(pattern);
        const repeatsNeeded = Math.ceil(desiredWidth / patternWidth);
        let tmp = new Array(repeatsNeeded).join(pattern);
        while (defaultFont.measureString(tmp) > desiredWidth) {
            tmp = tmp.substring(0, tmp.length - 1);
        }
        return tmp
    }
    this.__LineWithCallbacksPrototype = {
        /**
         * Set callback that will be called when line is redrawing. 
         * The arguments for the function are same as for select callback, 
         * but it can return dictionary whose 
         * key-value pairs will be setted as choice parameters
         * @param {Function} updateCallback 
         * @returns Line object
         */
        onUpdate: function onUpdate(updateCallback) {
            this.updateCallback = updateCallback;
            return this;
        },
        /**
         * Set callback that will be called when line is selected. 
         * The arguments for the function depends on line type.
         * Is callback return true than screen will not redrawed. 
         * It can be used to change to another mission screen.
         * @param {*} selectCallback 
         * @returns Line object
         */
        onSelected: function onSelected(selectCallback) {
            this.selectCallback = selectCallback;
            return this;
        }
    }
    // TODO: Add lines hiding?
    /**
     * Create empty line object
     */
    this.EmptyLine = function () {}
    this.EmptyLine.prototype.update = function update() {
        return { text: "" };
    }
    /**
     * Create line object that can`t be selected
     * @param {string} text 
     */
    this.LabelLine = function (text, alignment) {
        this.choiceObject = {
            text: ILib.$padTextAround(text, 32),
            unselectable: true,
            color: "whiteColor",
            alignment: alignment? alignment : "CENTER"
        }
    }
    this.LabelLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.LabelLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback();
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined &&
                    key !== "unselectable") {
                    if (key === "text")
                        this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
                    else
                        this.choiceObject[key] = value;
                }
            }
        }
        return this.choiceObject;
    };
    /**
     * Create line object that can be selected.
     * When selected onSelected callback will be called without any arguments
     * @param {string} text 
     */
    this.ButtonLine = function (text) {
        this.choiceObject = {
            text: ILib.$padTextOnSide(text, "right", 32),
            color: "cyanColor",
            alignment: "LEFT"
        };
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
    }
    this.ButtonLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.ButtonLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback();
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "color")
                        this.selectableColor = value;
                    else if (key === "text")
                        this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        return this.choiceObject;
    }
    this.ButtonLine.prototype.select = function select() {
        if (this.selectCallback)
            return this.selectCallback();
    }
    /**
     * Create line object that toggles between true and false when selected.
     * When selected onSelected callback will be called with new state as argument
     * @param {string} text 
     * @param {Boolean} initialState 
     */
    this.ToggleLine = function (text, initialState) {
        this.choiceObject = {
            color: "cyanColor"
        };
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
        this.text = text;
        this.state = !!initialState;
    }
    this.ToggleLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.ToggleLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback(this.state);
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "color")
                        this.selectableColor = value;
                    else if (key === "text")
                        this.text = value;
                    else if (key === "state")
                        this.state = !!value;
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        this.choiceObject.text =
            ILib.$padTextOnSide(this.text, "right", 24) +
            ILib.$padTextAround(" < " + (this.state ? "On" : "Off") + " > ", 8);
        return this.choiceObject;
    }
    this.ToggleLine.prototype.select = function select() {
        this.state = !this.state;
        if (this.selectCallback)
            return this.selectCallback(this.state);
    }
    /**
     * Create line object that cycles between the specified values when selected.
     * When selected onSelected callback will be called with new index and value as arguments
     * @param {string} text
     * @param {Array|Object.<string, any>} items If it is an array the values are 
     * the same as the labels, otherwise it is treated as a dictionary of labels to values
     * @param {any} [initialItem] 
     */
    this.CycleLine = function (text, items, initialItem) {
        const itemsIsArray = Array.isArray(items);
        const itemKeys = itemsIsArray ? items : Object.keys(items);
        let itemValues;
        if (itemsIsArray)
            itemValues = items
        else { // Object.values(items);
            itemValues = Array(itemKeys.length)
            for (let i = 0; i < itemKeys.length; i++)
                itemValues[i] = items[itemKeys[i]]
        }
        let initialIndex = itemKeys.indexOf(initialItem);
        if (initialIndex === -1)
            initialIndex = 0;
        this.choiceObject = {
            color: "cyanColor",
        };
        this.items = itemKeys;
        this.values = itemValues;
        this.currentIndex = initialIndex;
        this.text = text;
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
    }
    this.CycleLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.CycleLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback(this.currentIndex, this.values[this.currentIndex]);
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "text")
                        this.text = value;
                    else if (key === "currentIndex") {
                        this.currentIndex = value;
                    }
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        const currentItem = this.items[this.currentIndex];
        this.choiceObject.text =
            ILib.$padTextOnSide(this.text, "right", 24) +
            ILib.$padTextAround(" < " + currentItem + " > ", 8);
        return this.choiceObject;
    }
    this.CycleLine.prototype.select = function select() {
        this.currentIndex = (this.currentIndex + 1) % this.values.length;
        if (this.selectCallback)
            return this.selectCallback(this.currentIndex, this.values[this.currentIndex]);
    }
}).call(this); | 
                
                    | Scripts/route-planner-main.js | "use strict";
this.name = "RoutePlanner";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
/*************** Loading and saving variables ***************/
this.startUp = function () {
    this.$storedRoutes = [];
    const storedRoutes = JSON.parse(missionVariables.routePlanner_storedRoutes);
    if (storedRoutes)
        this.$storedRoutes = storedRoutes;
    const storedUserWaypoints = JSON.parse(missionVariables.routePlanner_storedUserWaypoints);
    if (storedUserWaypoints)
        this.$userWaypoints = storedUserWaypoints;
    const storedWaypointOrderMode = JSON.parse(missionVariables.routePlanner_waypointOrderMode);
    if (storedWaypointOrderMode)
        this.$currentWaypointOrderMode = storedWaypointOrderMode;
    const savedAppearanceParams = JSON.parse(missionVariables.routePlanner_appearanceParams);
    if (savedAppearanceParams)
        this.$appearance = savedAppearanceParams;
    const wpTypesList = Object.keys(this.$wpTypes)
    for(let i = 0; i < wpTypesList.length; i++)
    {
        const typeKey = wpTypesList[i];
        const savedIsUsed = missionVariables["routePlanner_wpType_isUsed" + typeKey];
        this.$wpTypes[typeKey].isUsed = savedIsUsed === null ? true : Boolean(Number(savedIsUsed));
    }    
    // log("RoutePlannerDebug", 
    //     "Loaded:".concat(
    //     "\nStored routes", JSON.stringify(this.$storedRoutes),
    //     "\nUser waypoints: ", JSON.stringify(this.$userWaypoints),
    //     "\nOrder mode: ", this.$currentWaypointOrderMode,
    //     "\nAppearence: ", JSON.stringify(this.$appearance),
    //     "\nEnd"
    // ));
    this.$waypointsOnCurrentRoute = [];
}
this.playerWillSaveGame = function (message) {
    const equipmentStatus = player.ship.equipmentStatus("EQ_ADVANCED_NAVIGATIONAL_ARRAY");
    if (equipmentStatus === "EQUIPMENT_OK" || equipmentStatus === "EQUIPMENT_DAMAGED") {
        missionVariables.routePlanner_storedRoutes = JSON.stringify(this.$storedRoutes);
        missionVariables.routePlanner_storedUserWaypoints = JSON.stringify(this.$userWaypoints);
        missionVariables.routePlanner_waypointOrderMode = JSON.stringify(this.$currentWaypointOrderMode)
        missionVariables.routePlanner_appearanceParams = JSON.stringify(this.$appearance);
        const wpTypesList = Object.keys(this.$wpTypes)
        for(let i = 0; i < wpTypesList.length; i++)
        {
            const typeKey = wpTypesList[i];
            missionVariables["routePlanner_wpType_isUsed" + typeKey] = Number(this.$wpTypes[typeKey].isUsed);
        }        
    }
    else {
        missionVariables.anlivel_routePlanner_storedRoutes = null; 
    }
}
/*************** Parameters ***************/
this.$waypointOrderModes = ["Manual", "Auto", "None"];
this.$currentWaypointOrderMode = "Auto";
this.$routeEditingEnabled = false;
this.$appearance = {
    userWaypointColor: "blueColor",
    userWaypointShape: "MARKER_X",
    routeLineColorByJumps: "cyanColor",
    routeLineColorByTime: "redColor"
}
/*************** Waypoint types ***************/
/**
 * @typedef {Object} WaypointType
 * @property {Boolean} isUsed
 * @property {Boolean} requiresMainStation
 * @property {string} nameKey
 * @property {Boolean} isTargetSystem
 */
/** @type {Object.<string, WaypointType>}*/
this.$wpTypes = {
    cargoContract:     { isUsed: true, requiresMainStation: true,  nameKey: "[RoutePlanner_wpType_cargo]" },
    parcelContract:    { isUsed: true, requiresMainStation: true,  nameKey: "[RoutePlanner_wpType_parcel]" },
    passengerContract: { isUsed: true, requiresMainStation: true,  nameKey: "[RoutePlanner_wpType_passenger]" }, 
    userSpecified:     { isUsed: true, requiresMainStation: false, nameKey: "[RoutePlanner_wpType_userSpecified]" }
}
/** @type {WaypointType}*/
this.$wpTypeTargetSystem = { 
    isUsed: true, 
    requiresMainStation: false, 
    nameKey: "[RoutePlanner_wpType_targetSystem]", 
    isTargetSystem: true
}
/*************** Interface ***************/
this.$startUpCompleted = false;
this.shipDockedWithStation = this.startUpComplete = function () {
    this.$startUpCompleted = true;
    const equipmentStatus = player.ship.equipmentStatus("EQ_ADVANCED_NAVIGATIONAL_ARRAY");
    if (equipmentStatus === "EQUIPMENT_OK" || equipmentStatus === "EQUIPMENT_DAMAGED") {
        this.$setInterfaceToStation();
    }
}
this.equipmentAdded = function (equipmentKey) {
    if (player.ship.docked && equipmentKey === "EQ_ADVANCED_NAVIGATIONAL_ARRAY") {
        this.$setInterfaceToStation();
    }
}
this.equipmentRemoved  = function (equipmentKey) {
    const station = player.ship.dockedStation;
    if (station && equipmentKey === "EQ_ADVANCED_NAVIGATIONAL_ARRAY") {
        station.setInterface("RoutePlanner_Interface", null);
    }
}
this.$setInterfaceToStation = function $setInterfaceToStation(){
    if (!this.$startUpCompleted) 
        return;
    if (!this.$interface) {
        let interfaceScript = worldScripts["RoutePlanner_Interface"];
        interfaceScript.$configureInterface();
        this.$interface = interfaceScript.$screens
        // log("RoutePlannerDebug", 
        //     "Interface initiated with:".concat(
        //     "\nUser waypoints: ", JSON.stringify(this.$userWaypoints),
        //     "\nOrder mode: ", this.$currentWaypointOrderMode,
        //     "\nAppearence: ", JSON.stringify(this.$appearance),
        //     "\nEnd"
        // )); 
    }
    const interfaceMain = this.$interface.main;
    const station = player.ship.dockedStation;
    station.setInterface("RoutePlanner_Interface", {
        title: "Route planning",
        category: "Ship Systems", // String.fromCharCode("0x1D") + "Ship Systems", // \u200b - zws
        summary: "Edit the current route, change the planning mode or other planner options",
        callback: interfaceMain.runScreen.bind(interfaceMain)
    });
}
/*************** Updating route in flight ***************/
// Route for previous galaxy makes no sense in new
this.playerEnteredNewGalaxy = function () {
    this.$storedRoutes = [];
    this.$userWaypoints = [];
    this.$storedUserWaypoints = [];
}
// When player launched without target system and have planed routes select first route automatically
this.shipWillLaunchFromStation = function () {
    if (player.ship.targetSystem === system.ID && this.$storedRoutes.length !== 0) {
        const autoRouteInfo = this.$removeFirstStoredRoute()
        const autoRoute = autoRouteInfo.route;
        const newTargetSystem = autoRoute[autoRoute.length - 1];
        player.ship.targetSystem = newTargetSystem;
        this.$waypointsOnCurrentRoute = [autoRouteInfo.endWaypointInfo];
        player.consoleMessage(
            expandMissionText("RoutePlanner_message_automatically-setted-destination", {
                nextWaypointName: System.systemNameForID(newTargetSystem)
            }), 5);
    }
}
/// TODO
this.shipExitedWitchspace = function () {
    if (this.$currentWaypointOrderMode !== "None" ) {
        const targetSystem = player.ship.targetSystem;
        if(targetSystem === system.ID) 
        {
            // log("RP TEST", JSON.stringify(this.$waypointsOnCurrentRoute))
            if (this.$storedRoutes.length !== 0) {
                const nextRouteInfo = this.$storedRoutes[0];
    
                const currentWpInfo = nextRouteInfo.startWaypointInfo;
                const wpRequiresMainStation = currentWpInfo.type.requiresMainStation;
                const wpType = currentWpInfo.type.nameKey;
                
                const nextWpInfo = nextRouteInfo.endWaypointInfo;
    
                let message = wpRequiresMainStation? 
                    "RoutePlanner_message_reached-waypoint_requires-main-station" : 
                    "RoutePlanner_message_reached-waypoint";
    
                player.consoleMessage(
                    expandMissionText(message, {
                        nextWaypointName: System.systemNameForID(nextWpInfo.ID),
                        waypointType: expandDescription(wpType)
                    }), 8);
            }
            else if(this.$waypointsOnCurrentRoute.length == 1)
            {
                const lastWp = this.$waypointsOnCurrentRoute[0];
                let message = lastWp.type.requiresMainStation? 
                    "RoutePlanner_message_reached-last-waypoint_requires-main-station" : 
                    "RoutePlanner_message_reached-last-waypoint";
                player.consoleMessage(
                    expandMissionText(message, {
                        waypointType: expandDescription(lastWp.type.nameKey)
                    }), 8);
            }
        }
        else if(this.$waypointsOnCurrentRoute.length > 1){
            const wp = this.$waypointsOnCurrentRoute[0];
            
            if(wp.ID === targetSystem) {
                let message = wp.type.requiresMainStation? 
                    "RoutePlanner_message_reached-midroute-waypoint_requires-main-station" : 
                    "RoutePlanner_message_reached-midroute-waypoint";
                player.consoleMessage(
                    expandMissionText(message, {
                        waypointType: expandDescription(wp.type.nameKey)
                    }), 8);
                
                    this.$waypointsOnCurrentRoute.shift();
            }
        }        
    }
    const userWpIndex = this.$userWaypoints.indexOf(system.ID);
    if (userWpIndex !== -1) {
        this.$userWaypoints.splice(userWpIndex, 1);
        this.$redrawUserWaypoints(
            this.$userWaypoints, 
            this.$appearance.userWaypointColor, 
            this.$appearance.userWaypointShape
        );
    }
}
/*************** Operation with waypoints ***************/
/**
 * @typedef {Object} WaypointInfo
 * @property {Number} ID
 * @property {WaypointType} type
 * @property {Number|null} eta
 */
/** @type {WaypointInfo[]} */
this.$userWaypoints = [];
this.$collectWaypoints = function $collectWaypoints() {
    const playerShip = player.ship;
    const targetSystem = playerShip.targetSystem;
    const seenSystems = {/* wpDestination0: outIndex0, ... */}; 
    const currentRouteSystems = {/* wpDestination0: routeWpIndex, ... */}; 
    const plannedWps = [];
    let plannedWpsIndex = 0;
    const currentRouteWps = []; // waypoints that already on current route
    let currentRouteWpsIndex = 0;
    let targetSystemWp = { // waypoints that on current targetSystem
        ID: targetSystem,
        type: this.$wpTypeTargetSystem,
        eta: undefined
    };
    // do this to remove waypoints that are already on the current route
    const currentRouteInfo = system.info.routeToSystem(
        System.infoForSystem(galaxyNumber, targetSystem),
        playerShip.routeMode);
    if (currentRouteInfo) {
        let i = currentRouteInfo.route.length;
        while (i--) {
            currentRouteSystems[currentRouteInfo.route[i]] = i;
        }
    }
    
    const wpCollections = [
        {collection: playerShip.contracts,  type: this.$wpTypes.cargoContract},
        {collection: playerShip.parcels,    type: this.$wpTypes.parcelContract},
        {collection: playerShip.passengers, type: this.$wpTypes.passengerContract}
    ];
    const userWp = this.$userWaypoints.map(function (systemId) {
        return { destination: systemId }
    });
    wpCollections.push({collection: userWp, type: this.$wpTypes.userSpecified});
    
    let wpCollectionIndex = wpCollections.length;
    while (wpCollectionIndex--) {
        const wpCollection = wpCollections[wpCollectionIndex].collection;
        const wpType = wpCollections[wpCollectionIndex].type;
        if(!wpType.isUsed)
            continue;
        let i = wpCollection.length;
        while (i--) {
            let wp = wpCollection[i];
            let wpEta = wp.eta + clock.seconds; // Not sure if it can be called now eta, but whatever
            let wpDestination = wp.destination;
            let indexInCurrentRoute = currentRouteSystems[wpDestination];
            let wpLiesOnCurrentRoute = indexInCurrentRoute !== undefined;
            if (seenSystems[wpDestination] === undefined) {
                let storedWaypoint = {
                    ID: wpDestination,
                    type: wpType,
                    eta: wpEta
                };
                if(wpLiesOnCurrentRoute)
                {
                    if(targetSystemWp.ID === wpDestination)
                    {
                        if(targetSystemWp.type.isTargetSystem) // it target system type then just replace
                        {
                            targetSystemWp = storedWaypoint;
                        }
                        else if(targetSystemWp.type !== wpType)
                        {
                            seenWp.type = {
                                isUsed: targetSystemWp.isUsed || wpType.isUsed,
                                requiresMainStation: targetSystemWp.requiresMainStation || wpType.requiresMainStation,
                                nameKey: "[RoutePlanner_wpType_several]"
                            }
                        }
                    }
                    storedWaypoint.indexInCurrentRoute = indexInCurrentRoute;
                    seenSystems[wpDestination] = currentRouteWpsIndex;
                    currentRouteWps[currentRouteWpsIndex] = storedWaypoint;
                    currentRouteWpsIndex++;
                }
                else
                {
                    seenSystems[wpDestination] = plannedWpsIndex;
                    plannedWps[plannedWpsIndex] = storedWaypoint;
                    plannedWpsIndex++;
                }                
            }
            else 
            {   
                let wpIndex = seenSystems[wpDestination];
                
                let seenWp = wpLiesOnCurrentRoute? currentRouteWps[wpIndex] : plannedWps[wpIndex];
                let seenWpType = seenWp.type;
                if(seenWpType !== wpType)
                {
                    seenWp.type = {
                        isUsed: seenWpType.isUsed || wpType.isUsed,
                        requiresMainStation: seenWpType.requiresMainStation || wpType.requiresMainStation,
                        nameKey: "[RoutePlanner_wpType_several]"
                    }
                }
                
                seenWp.eta = seenWp.eta < wpEta? seenWp.eta : wpEta;
            }
        }  
    }
    currentRouteWps.sort(function (a, b) {
        return a.indexInCurrentRoute < b.indexInCurrentRoute;
    });
    return {onRoute: currentRouteWps, notOnRoute: plannedWps, targetSystem: targetSystemWp};
}
this.$getUserWaypoints = function $getUserWaypoints() {
    const that = this;
    const targetSystem = player.ship.targetSystem;   
    const userWaypoints = this.$userWaypoints;
    let startI;
    let targetSystemWp; // waypoints that on current targetSystem
    if(targetSystem && userWaypoints[0] === targetSystem) {
        startI = 1;
        targetSystemWp = { 
            ID:  targetSystem,
            type: this.$wpTypeTargetSystem,
            eta: undefined
        };
    }
    else {
        startI = 0;
        targetSystemWp = {
            ID: userWaypoints[0], 
            type: this.$wpTypes.userSpecified
        }
    }     
    const plannedWps = [];
    for(let i = startI; i < userWaypoints.length; i++) {
        plannedWps.push({
            ID: userWaypoints[i], 
            type: that.$wpTypes.userSpecified
        });
    }
    return {onRoute: [targetSystemWp], notOnRoute: plannedWps, targetSystem: targetSystemWp};
}
this.$recalculateRoutes = function $recalculateRoutes() {
    let newRoutesInfo = undefined;
    const storedRoutes = this.$storedRoutes;
    const currentWaypointOrderMode = this.$currentWaypointOrderMode;
    const targetSystem = player.ship.targetSystem;
    const routeMode = player.ship.routeMode;
    if (currentWaypointOrderMode === "None" || routeMode === "OPTIMIZED_BY_NONE") {
        this.$redrawRoutes([], lineColor);
        this.$redrawUserWaypoints([], this.$appearance.userWaypointColor, this.$appearance.userWaypointShape);
        return 
    }
        
    let waypoints;
    // There was a check to see if it was possible not to recalculate the path, 
    // but it became too complicated. Maybe should return it later
    if (true) {
        this.$prevRouteMode = routeMode;
        const autoOrderMode = currentWaypointOrderMode === "Auto";
        
        if(autoOrderMode)
        {
            waypoints = this.$collectWaypoints();
            newRoutesInfo = this.$routeWithCheapestInsertion(waypoints.targetSystem, waypoints.notOnRoute, routeMode);
        }
        else
        {
            waypoints = this.$getUserWaypoints();
            newRoutesInfo = this.$routeKeepingOrder(waypoints.targetSystem, waypoints.notOnRoute, routeMode);
        }
    }
    this.$waypointsOnCurrentRoute = waypoints.onRoute;
    const lineColor = routeMode === "OPTIMIZED_BY_JUMPS" ?
        this.$appearance.routeLineColorByJumps :
        this.$appearance.routeLineColorByTime;
    this.$redrawRoutes(newRoutesInfo, lineColor);
    this.$redrawUserWaypoints(this.$userWaypoints, this.$appearance.userWaypointColor, this.$appearance.userWaypointShape);
}
this.playerEnteredContract = function () { this.$recalculateRoutes() };
this.guiScreenChanged = function (to, from) {
    if (this.$routeEditingEnabled) {
        const fromChartScreen = from === "GUI_SCREEN_SHORT_RANGE_CHART" || from === "GUI_SCREEN_LONG_RANGE_CHART";
        const toChartScreen = to === "GUI_SCREEN_SHORT_RANGE_CHART" || to === "GUI_SCREEN_LONG_RANGE_CHART";
        const toSystemDataScreen = to === "GUI_SCREEN_SYSTEM_DATA";
        if (fromChartScreen && toSystemDataScreen) {
            const editScreen = this.$interface.editRouteSystemAction;
            editScreen.runScreen.call(editScreen, player.ship.infoSystem, this.$currentWaypointOrderMode);
        }
        else if ((fromChartScreen && toChartScreen) || mission.screenID) { }
        else {
            this.$routeEditingEnabled = false;
        }
    }
    // Start periodicaly check if route mode or cursor position changed if user on chart screen
    if (guiScreen === "GUI_SCREEN_SHORT_RANGE_CHART" ||
        guiScreen === "GUI_SCREEN_LONG_RANGE_CHART") {
        this.$prevRouteMode = player.ship.routeMode;
        this.$prevCursorCoordinates = player.ship.cursorCoordinates;
        this.$cursorMoving = false;
        this.$routeUndrawed = true;
        this.$checkChartInterfaceChangesTimer.start();
    }
    else {
        this.$checkChartInterfaceChangesTimer.stop();
        if ((from === "GUI_SCREEN_SHORT_RANGE_CHART" ||
            from === "GUI_SCREEN_LONG_RANGE_CHART") &&
            this.$routeUndrawed === false) {
            this.$eraseStoredRoutes();
        }
    }
}
this.$checkChartInterfaceChangesTimer = new Timer(this, function $checkChartInterfaceChangesTimer() {
    const cursorCoordinates = player.ship.cursorCoordinates;
    if (cursorCoordinates.x != this.$prevCursorCoordinates.x ||
        cursorCoordinates.y != this.$prevCursorCoordinates.y)
        this.$cursorMoving = true;
    else
        this.$cursorMoving = false;
    if (player.ship.routeMode != this.$prevRouteMode) {
        this.$recalculateRoutes();
    }
    else if (this.$cursorMoving && this.$routeUndrawed === false) {
        this.$eraseStoredRoutes();
        this.$routeUndrawed = true;
    } else if (this.$cursorMoving == false && this.$routeUndrawed) {
        this.$routeUndrawed = false;
        this.$recalculateRoutes();
    }
    this.$prevCursorCoordinates = cursorCoordinates;
}, -1, 0.25);
/**
 * Builds hopefully approximate optimal route from starting system thought waypoints
 * @param {WaypointInfo} startSystem
 * @param {WaypointInfo[]} waypoints 
 * @param {"OPTIMIZED_BY_NONE"|"OPTIMIZED_BY_JUMPS"|"OPTIMIZED_BY_TIME"} routeMode 
 * @returns {RouteInfo[]}
 */
this.$routeWithCheapestInsertion = function $routeWithCheapestInsertion(startSystem, waypoints, routeMode) {
    if (routeMode === "OPTIMIZED_BY_NONE" || waypoints.length === 0) return [];
    const measurement = routeMode === "OPTIMIZED_BY_JUMPS" ? "distance" : "time";
    waypoints.sort((function (a, b) {
        return this.$memoizedRoutes.routeCost(startSystem.ID, b.ID)[measurement] -
            this.$memoizedRoutes.routeCost(startSystem.ID, a.ID)[measurement];
    }).bind(this));
    const waypointsCount = waypoints.length;
    const bestRoute = [startSystem];
    const noRoute = { route: { length: Infinity }, distance: Infinity, time: Infinity };
    //bestRoute[0] = { ID: startSystem.ID };
    for (let i = 0; i < waypointsCount; i++) {
        const newWp = waypoints[i];
        if (!newWp.ID) break;
        const endPos = bestRoute.length - 1;
        let bestInsertPos, bestCostDifference, bestNewCostStart, bestNewCostEnd = null;
        if (true) { // check cost of inserting in end
            const endRouteWp = bestRoute[endPos];
            const cost = this.$memoizedRoutes.routeCost(endRouteWp.ID, newWp.ID);
            let costDifference = cost[measurement];// * 2*(1 + i/waypointsCount);//* 2;
            bestInsertPos = endPos + 1;
            bestCostDifference = costDifference;
            bestNewCostStart = cost;
        }
        for (let j = 0; j < endPos; j++) {
            const startWp = bestRoute[j];
            const endWp = bestRoute[j + 1];
            const oldCost = this.$memoizedRoutes.routeCost(startWp.ID, endWp.ID);
            const newCostStart = this.$memoizedRoutes.routeCost(startWp.ID, newWp.ID);
            const newCostEnd = this.$memoizedRoutes.routeCost(newWp.ID, endWp.ID);
            let costDifference = (newCostStart[measurement] + newCostEnd[measurement]) - oldCost[measurement];
            if (costDifference < bestCostDifference) {
                bestInsertPos = j + 1;
                bestCostDifference = costDifference;
                bestNewCostStart = newCostStart;
                bestNewCostEnd = newCostEnd;
            }
        }
        // Had plans to posibillity to add transitions between systems that cann`t be done with usual witchjump
        // Maybe someday I`ll do it. Maybe
        //
        // if(bestNewCostStart.through)
        //     if(bestEndCostStart && bestEndCostStart.through)
        //         bestRoute.splice(bestInsertPos, 0, bestNewCostStart.through, newWp, bestEndCostStart.through);
        //     else
        //         bestRoute.splice(bestInsertPos, 0, bestNewCostStart.through, newWp);
        // else
        //     if(bestEndCostStart && bestEndCostStart.through)
        //         bestRoute.splice(bestInsertPos, 0, newWp, bestEndCostStart.through);
        //     else
        //         bestRoute.splice(bestInsertPos, 0, newWp);    
        bestRoute.splice(bestInsertPos, 0, newWp);
    }
    const partialRoutesCount = bestRoute.length - 1;
    const partialRoutes = new Array(partialRoutesCount);
    let startWaypointInfo = bestRoute[0];
    let aInfo, bInfo = System.infoForSystem(galaxyNumber, bestRoute[0].ID);
    for (let i = 1; i < bestRoute.length; i++) {
        aInfo = bInfo;
        bInfo = System.infoForSystem(galaxyNumber, bestRoute[i].ID);
        let routeInfo = aInfo.routeToSystem(bInfo, routeMode);
        routeInfo.startWaypointInfo = bestRoute[i-1];
        routeInfo.endWaypointInfo = bestRoute[i];
        partialRoutes[i - 1] = routeInfo;
    }
    return partialRoutes;
}
/**
 * Builds a route from the starting system through waypoints in their exact order
 * @param {WaypointInfo} startSystem
 * @param {WaypointInfo[]} waypoints 
 * @param {"OPTIMIZED_BY_NONE"|"OPTIMIZED_BY_JUMPS"|"OPTIMIZED_BY_TIME"} routeMode 
 * @returns {RouteInfo[]}
 */
this.$routeKeepingOrder = function $routeKeepingOrder(startSystem, waypoints, routeMode) {
    if (routeMode === "OPTIMIZED_BY_NONE" || waypoints.length === 0) return [];
    const partialRoutesCount = waypoints.length;
    const partialRoutes = new Array(partialRoutesCount);
    let previusWaypoint, currentWaypoint = startSystem;
    let aInfo, bInfo = System.infoForSystem(galaxyNumber, startSystem.ID);
    for (let i = 0; i < partialRoutesCount; i++) {
        previusWaypoint = currentWaypoint;
        currentWaypoint = waypoints[i];
        aInfo = bInfo;
        bInfo = System.infoForSystem(galaxyNumber, currentWaypoint.ID);
        let routeInfo = aInfo.routeToSystem(bInfo, routeMode);
        routeInfo.startWaypointInfo = previusWaypoint;
        routeInfo.endWaypointInfo = currentWaypoint;
        partialRoutes[i] = aInfo.routeToSystem(bInfo, routeMode);
    }
    return partialRoutes;
}
this.$memoizedRoutes = {
    routeCost: function (from, to) {
        if (to === null || to === undefined || from === to)
            return { distance: 0, time: 0, through: null };
        // routes are symmetric, right?
        if (from <= to) {
            a = from;
            b = to
        } else {
            a = to;
            b = from;
        }
        const a = from < to ? from : to;
        const b = a === from ? to : from;
        let aCosts = this.$costsMatrix[a];
        if (aCosts === undefined) {
            aCosts = this.$costsMatrix[a] = {};
        }
        let routeCost = aCosts[b];
        if (routeCost === undefined) {
            const aSystemInfo = System.infoForSystem(galaxyNumber, a);
            const bSystemInfo = System.infoForSystem(galaxyNumber, b);
            const routeInfoByJumps = aSystemInfo.routeToSystem(bSystemInfo, "OPTIMIZED_BY_JUMPS");
            const routeInfoByTime = aSystemInfo.routeToSystem(bSystemInfo, "OPTIMIZED_BY_TIME");
            if (routeInfoByJumps == null)
                routeCost = aCosts[b] = null;
            else {
                routeCost = aCosts[b] = {
                    distance: routeInfoByJumps.distance,
                    timeWhenDistance: routeInfoByJumps.time,
                    time: routeInfoByTime.time,
                    distanceWhenTime: routeInfoByTime.distance,
                    through: null
                };
            }
        }
        return routeCost;
    },
    $costsMatrix: {
        /*
        systemNumberFrom: {
            systemNumberTo: {
                distance: 0
                timeWhenDistance: 0, // time when optimized by distance
                time: 0
                distanceWhenTime: 0, // distance when optimized by time
                through: null
            }
        }
        */
    }
}
/*************** Chart drawing operations ***************/
/**
 * Remove stored routes from chart but keep them stored
 */
this.$eraseStoredRoutes = function () {
    for (let i = 0; i < this.$storedRoutes.length; i++) {
        this.$eraseRoute(this.$storedRoutes[i]);
    }
}
/**
 * Remove stored routes from chart, draw and store new routes.
 * If need remove current routes and do not draw new pass [] as newRoutes
 * @param {RouteInfo[]} newRoutes 
 */
this.$redrawRoutes = function (newRoutes, lineColor) {
    if (newRoutes) {
        for (let i = 0; i < this.$storedRoutes.length; i++) {
            this.$eraseRoute(this.$storedRoutes[i]);
        }
    } else {
        newRoutes = this.$storedRoutes;
    }
    for (let i = 0; i < newRoutes.length; i++) {
        /*let a = i;
        new Timer(this, function() {
            this.$drawRoute(newRoutes[a]);
        }, i);
        */
        this.$drawRoute(newRoutes[i], lineColor);
    }
    this.$storedRoutes = newRoutes;
}
/**
 * Remove first stored route, erase it from chart and return
 * @returns {RouteInfo} Removed route
 */
this.$removeFirstStoredRoute = function () {
    const route = this.$storedRoutes.shift();
    this.$eraseRoute(route);
    return route;
}
this.$drawRoute = function (routeInfo, lineColor) {
    for (var i = 0; i < routeInfo.route.length - 1; i++) {
        if (routeInfo.route[i] < routeInfo.route[i + 1]) {
            SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i], routeInfo.route[i + 1], 2, "link_color", lineColor);
        } else {
            SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i + 1], routeInfo.route[i], 2, "link_color", lineColor);
        }
    }
}
this.$eraseRoute = function (routeInfo) {
    mission.unmarkSystem({
        system: routeInfo.route[0],
        name: this.name
    });
    for (var i = 0; i < routeInfo.route.length - 1; i++) {
        if (routeInfo.route[i] < routeInfo.route[i + 1]) {
            SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i], routeInfo.route[i + 1], 2, "link_color", null);
        } else {
            SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i + 1], routeInfo.route[i], 2, "link_color", null);
        }
    }
}
this.$storedUserWaypoints = [];
/**
 * Erase stored user waypoints from chart, draw and store new.
 * @param {WaypointInfo[]} newUserWaypoints 
 * @param {string} userWaypointColor 
 * @param {string} userWaypointShape 
 */
this.$redrawUserWaypoints = function $redrawUserWaypoints (newUserWaypoints, userWaypointColor, userWaypointShape) {
    const storedUserWaypoints = this.$storedUserWaypoints;
    for(let i = 0; i < storedUserWaypoints.length; i++) {
        mission.unmarkSystem({
            system: storedUserWaypoints[i],
            name: this.name
        });
    }
    this.$storedUserWaypoints = (newUserWaypoints || []).slice();
    for(let i = 0; i < storedUserWaypoints.length; i++) {
        mission.markSystem({
            system: newUserWaypoints[i],
            name: this.name,
            markerColor: userWaypointColor,
            markerShape: userWaypointShape
        });
    }
} |