WME Straighten Up!

Straighten selected WME segment(s) by aligning along straight line between two end points and removing geometry nodes.

当前为 2020-07-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WME Straighten Up!
// @namespace   https://greasyfork.org/users/166843
// @version      2020.07.27.01
// @description  Straighten selected WME segment(s) by aligning along straight line between two end points and removing geometry nodes.
// @author       dBsooner
// @include     /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @require     https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant        none
// @license      GPLv3
// ==/UserScript==

// Original credit to jonny3D and impulse200

/* global document, localStorage, window, $, MutationObserver, performance, I18n, GM_info, W, WazeWrap */

const ALERT_UPDATE = true,
    DEBUG = false,
    LOAD_BEGIN_TIME = performance.now(),
    // SCRIPT_AUTHOR = GM_info.script.author,
    SCRIPT_FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=289116',
    SCRIPT_GF_URL = 'https://greasyfork.org/en/scripts/388349-wme-straighten-up',
    SCRIPT_NAME = GM_info.script.name.replace('(beta)', 'β'),
    SCRIPT_VERSION = GM_info.script.version,
    SCRIPT_VERSION_CHANGES = ['<b>NEW:</b> Shortcut to run Straighten Up!',
        '<b>CHANGE:</b> WME map object references.'],
    SETTINGS_STORE_NAME = 'WMESU',
    _timeouts = { bootstrap: undefined, saveSettingsToStorage: undefined },
    _editPanelObserver = new MutationObserver(mutations => {
        if ((W.selectionManager.getSegmentSelection().segments.length === 0) || ($('#WMESU-div-button').length > 0))
            return;
        const addedChildren = mutations.filter(mutation => (mutation.type === 'childList')).filter(mutatedChild => (mutatedChild.addedNodes.length > 0));
        if ((addedChildren.filter(
            addedChild => (
                (addedChild.addedNodes[0].className
                    && (addedChild.addedNodes[0].className.indexOf('segment') > -1)
                )
                || (addedChild.addedNodes[0].firstElementChild && addedChild.addedNodes[0].firstElementChild.className
                    && (addedChild.addedNodes[0].firstElementChild.className.indexOf('segment') > -1)
                )
            )
        ).length > 0)) {
            if ($('#WMESU-div-button').length === 0)
                insertSimplifyStreetGeometryButtons();
        }
    });
let _settings = {};

async function loadSettingsFromStorage() {
    const defaultSettings = {
            conflictingNames: 'warning',
            longJnMove: 'warning',
            microDogLegs: 'warning',
            nonContinuousSelection: 'warning',
            sanityCheck: 'warning',
            runStraightenUpShortcut: '',
            lastSaved: 0,
            lastVersion: undefined
        },
        loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
    _settings = $.extend({}, defaultSettings, loadedSettings);
    const serverSettings = await WazeWrap.Remote.RetrieveSettings(SETTINGS_STORE_NAME);
    if (serverSettings && (serverSettings.lastSaved > _settings.lastSaved))
        $.extend(_settings, serverSettings);
    _timeouts.saveSettingsToStorage = window.setTimeout(saveSettingsToStorage, 5000);
    return Promise.resolve();
}

function saveSettingsToStorage() {
    checkTimeout({ timeout: 'saveSettingsToStorage' });
    if (localStorage) {
        _settings.lastVersion = SCRIPT_VERSION;
        _settings.lastSaved = Date.now();
        localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(_settings));
        WazeWrap.Remote.SaveSettings(SETTINGS_STORE_NAME, _settings);
        logDebug('Settings saved.');
    }
}

function checkShortcutChanged() {
    let keys = '';
    const { shortcut } = W.accelerators.Actions.runStraightenUpShortcut;
    if (shortcut) {
        if (shortcut.altKey)
            keys += 'A';
        if (shortcut.shiftKey)
            keys += 'S';
        if (shortcut.ctrlKey)
            keys += 'C';
        if (keys !== '')
            keys += '+';
        if (shortcut.keyCode)
            keys += shortcut.keyCode;
    }
    else {
        keys = '';
    }
    if (_settings.runStraightenUpShortcut !== keys) {
        _settings.runStraightenUpShortcut = keys;
        saveSettingsToStorage();
    }
}

function showScriptInfoAlert() {
    if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
        let releaseNotes = '';
        releaseNotes += `<p>${I18n.t('wmesu.common.WhatsNew')}:</p>`;
        if (SCRIPT_VERSION_CHANGES.length > 0) {
            releaseNotes += '<ul>';
            for (let idx = 0; idx < SCRIPT_VERSION_CHANGES.length; idx++)
                releaseNotes += `<li>${SCRIPT_VERSION_CHANGES[idx]}`;
            releaseNotes += '</ul>';
        }
        else {
            releaseNotes += `<ul><li>${I18n.t('wmesu.common.NothingMajor')}</ul>`;
        }
        WazeWrap.Interface.ShowScriptUpdate(SCRIPT_NAME, SCRIPT_VERSION, releaseNotes, SCRIPT_GF_URL, SCRIPT_FORUM_URL);
    }
}

function checkTimeout(obj) {
    if (obj.toIndex) {
        if (_timeouts[obj.timeout] && (_timeouts[obj.timeout][obj.toIndex] !== undefined)) {
            window.clearTimeout(_timeouts[obj.timeout][obj.toIndex]);
            _timeouts[obj.timeout][obj.toIndex] = undefined;
        }
    }
    else {
        if (_timeouts[obj.timeout] !== undefined)
            window.clearTimeout(_timeouts[obj.timeout]);
        _timeouts[obj.timeout] = undefined;
    }
}

function log(message) { console.log('WME-SU:', message); }
function logError(message) { console.error('WME-SU:', message); }
function logWarning(message) { console.warn('WME-SU:', message); }
function logDebug(message) {
    if (DEBUG)
        console.log('WME-SU:', message);
}

// рассчитаем пересчечение перпендикуляра точки с наклонной прямой
// Calculate the intersection of the perpendicular point with an inclined line
function getIntersectCoord(a, b, c, d) {
    // второй вариант по-проще: http://rsdn.ru/forum/alg/2589531.hot
    const r = [2];
    r[1] = -1.0 * (c * b - a * d) / (a * a + b * b);
    r[0] = (-r[1] * (b + a) - c + d) / (a - b);
    return { x: r[0], y: r[1] };
}

// определим направляющие
// Define guides
function getDeltaDirect(a, b) {
    let d = 0.0;
    if (a < b)
        d = 1.0;
    else if (a > b)
        d = -1.0;
    return d;
}

function checkNameContinuity(selectedFeatures) {
    const streetIds = [];
    for (let idx = 0; idx < selectedFeatures.length; idx++) {
        if (idx > 0) {
            if ((selectedFeatures[idx].model.attributes.primaryStreetID > 0) && (streetIds.indexOf(selectedFeatures[idx].model.attributes.primaryStreetID) > -1))
                // eslint-disable-next-line no-continue
                continue;
            if (selectedFeatures[idx].model.attributes.streetIDs.length > 0) {
                let included = false;
                for (let idx2 = 0; idx2 < selectedFeatures[idx].model.attributes.streetIDs.length; idx2++) {
                    if (streetIds.indexOf(selectedFeatures[idx].model.attributes.streetIDs[idx2]) > -1) {
                        included = true;
                        break;
                    }
                }
                if (included === true)
                    // eslint-disable-next-line no-continue
                    continue;
                else
                    return false;
            }
            return false;
        }
        if (idx === 0) {
            if (selectedFeatures[idx].model.attributes.primaryStreetID > 0)
                streetIds.push(selectedFeatures[idx].model.attributes.primaryStreetID);
            if (selectedFeatures[idx].model.attributes.streetIDs.length > 0)
                selectedFeatures[idx].model.attributes.streetIDs.forEach(streetId => { streetIds.push(streetId); });
        }
    }
    return true;
}

function distanceBetweenPoints(lon1, lat1, lon2, lat2, measurement) {
    // eslint-disable-next-line no-nested-ternary
    const multiplier = measurement === 'meters' ? 1000 : measurement === 'miles' ? 0.621371192237334 : measurement === 'feet' ? 3280.8398950131 : 1;
    lon1 *= 0.017453292519943295; // 0.017453292519943295 = Math.PI / 180
    lat1 *= 0.017453292519943295;
    lon2 *= 0.017453292519943295;
    lat2 *= 0.017453292519943295;
    // 12742 = Diam of earth in km (2 * 6371)
    return 12742 * Math.asin(Math.sqrt(((1 - Math.cos(lat2 - lat1)) + (1 - Math.cos(lon2 - lon1)) * Math.cos(lat1) * Math.cos(lat2)) / 2)) * multiplier;
}

function checkForMicroDogLegs(distinctNodes, singleSegmentId) {
    if (!distinctNodes || (distinctNodes.length < 1))
        return false;
    const nodesChecked = [],
        nodesObjArr = W.model.nodes.getByIds(distinctNodes);
    if (!nodesObjArr || (nodesObjArr.length < 1))
        return false;
    const checkGeoComp = (geoComp, node4326) => {
        const testNode4326 = WazeWrap.Geometry.ConvertTo4326(geoComp.x, geoComp.y);
        if ((node4326.lon !== testNode4326.lon) || (node4326.lat !== testNode4326.lat)) {
            if (distanceBetweenPoints(node4326.lon, node4326.lat, testNode4326.lon, testNode4326.lat, 'meters') < 2)
                return false;
        }
        return true;
    };
    for (let idx = 0; idx < nodesObjArr.length; idx++) {
        if (nodesChecked.indexOf(nodesObjArr[idx]) === -1) {
            nodesChecked.push(nodesObjArr[idx]);
            const segmentsObjArr = W.model.segments.getByIds(nodesObjArr[idx].getSegmentIds()) || [],
                node4326 = WazeWrap.Geometry.ConvertTo4326(nodesObjArr[idx].geometry.x, nodesObjArr[idx].geometry.y);
            for (let idx2 = 0; idx2 < segmentsObjArr.length; idx2++) {
                const segObj = segmentsObjArr[idx2];
                if (!singleSegmentId
                    || (singleSegmentId && (segObj.attributes.id === singleSegmentId))) {
                    if (!segObj.geometry.components.every(geoComp => checkGeoComp(geoComp, node4326)))
                        return true;
                }
            }
        }
    }
    return false;
}

function doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, longJnMoveContinue, passedObj) {
    const selectedFeatures = W.selectionManager.getSelectedFeatures(),
        segmentSelection = W.selectionManager.getSegmentSelection();
    if (longJnMoveContinue && (passedObj !== undefined)) {
        const { segmentsToRemoveGeometryArr } = passedObj,
            { nodesToMoveArr } = passedObj,
            { distinctNodes } = passedObj,
            { endPointNodeIds } = passedObj;
        logDebug(`${I18n.t('wmesu.log.StraighteningSegments')}: ${distinctNodes.join(', ')} (${distinctNodes.length})`);
        logDebug(`${I18n.t('wmesu.log.EndPoints')}: ${endPointNodeIds.join(' & ')}`);
        if (segmentsToRemoveGeometryArr && (segmentsToRemoveGeometryArr.length > 0)) {
            const UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
            segmentsToRemoveGeometryArr.forEach(segment => {
                W.model.actionManager.add(new UpdateSegmentGeometry(segment.model, segment.model.geometry, segment.newGeo));
                logDebug(`${I18n.t('wmesu.log.RemovedGeometryNodes')} # ${segment.model.attributes.id}`);
            });
        }
        if (nodesToMoveArr && (nodesToMoveArr.length > 0)) {
            const MoveNode = require('Waze/Action/MoveNode');
            let straightened = false;
            nodesToMoveArr.forEach(node => {
                if ((Math.abs(node.geometry.x - node.nodeGeo.x) > 0.00000001) || (Math.abs(node.geometry.y - node.nodeGeo.y) > 0.00000001)) {
                    logDebug(`${I18n.t('wmesu.log.MovingJunctionNode')} # ${node.node.attributes.id} `
                        + `- ${I18n.t('wmesu.common.From')}: ${node.geometry.x},${node.geometry.y} - `
                        + `${I18n.t('wmesu.common.To')}: ${node.nodeGeo.x},${node.nodeGeo.y}`);
                    W.model.actionManager.add(new MoveNode(node.node, node.geometry, node.nodeGeo, node.connectedSegObjs, {}));
                    straightened = true;
                }
            });
            if (!straightened) {
                logDebug(I18n.t('wmesu.log.AllNodesStraight'));
                return WazeWrap.Alerts.info(SCRIPT_NAME, I18n.t('wmesu.log.AllNodesStraight'));
            }
        }
    }
    else if (selectedFeatures.length > 1) {
        const segmentsToRemoveGeometryArr = [],
            nodesToMoveArr = [];
        if ((selectedFeatures.length > 10) && !sanityContinue) {
            if (_settings.sanityCheck === 'error')
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.TooManySegments'));
            if (_settings.sanityCheck === 'warning') {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmesu.prompts.SanityCheckConfirm'),
                    () => { doStraightenSegments(true, false, false, false, false, undefined); },
                    () => { },
                    I18n.t('wmesu.common.Yes'),
                    I18n.t('wmesu.common.No')
                );
            }
        }
        sanityContinue = true;
        if ((segmentSelection.multipleConnectedComponents === true) && !nonContinuousContinue) {
            if (_settings.nonContinuousSelection === 'error')
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.NonContinuous'));
            if (_settings.nonContinuousSelection === 'warning') {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmesu.prompts.NonContinuousConfirm'),
                    () => { doStraightenSegments(sanityContinue, true, false, false, false, undefined); },
                    () => { },
                    I18n.t('wmesu.common.Yes'),
                    I18n.t('wmesu.common.No')
                );
            }
        }
        nonContinuousContinue = true;
        if (_settings.conflictingNames !== 'nowarning') {
            const continuousNames = checkNameContinuity(selectedFeatures);
            if (!continuousNames && !conflictingNamesContinue && (_settings.conflictingNames === 'error'))
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.ConflictingNames'));
            if (!continuousNames && !conflictingNamesContinue && (_settings.conflictingNames === 'warning')) {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmesu.prompts.ConflictingNamesConfirm'),
                    () => { doStraightenSegments(sanityContinue, nonContinuousContinue, true, false, false, undefined); },
                    () => { },
                    I18n.t('wmesu.common.Yes'),
                    I18n.t('wmesu.common.No')
                );
            }
        }
        conflictingNamesContinue = true;
        const allNodeIds = [],
            dupNodeIds = [];
        let endPointNodeIds,
            longMove = false;
        for (let idx = 0; idx < selectedFeatures.length; idx++) {
            allNodeIds.push(selectedFeatures[idx].model.attributes.fromNodeID);
            allNodeIds.push(selectedFeatures[idx].model.attributes.toNodeID);
            if (selectedFeatures[idx].model.type === 'segment') {
                const newGeo = selectedFeatures[idx].model.geometry.clone();
                // Remove the geometry nodes
                if (newGeo.components.length > 2) {
                    newGeo.components.splice(1, newGeo.components.length - 2);
                    newGeo.components[0].calculateBounds();
                    newGeo.components[1].calculateBounds();
                    segmentsToRemoveGeometryArr.push({ model: selectedFeatures[idx].model, geometry: selectedFeatures[idx].model.geometry, newGeo });
                }
            }
        }
        allNodeIds.forEach((nodeId, idx) => {
            if (allNodeIds.indexOf(nodeId, idx + 1) > -1) {
                if (dupNodeIds.indexOf(nodeId) === -1)
                    dupNodeIds.push(nodeId);
            }
        });
        const distinctNodes = [...new Set(allNodeIds)];
        if (!microDogLegsContinue && (checkForMicroDogLegs(distinctNodes, undefined) === true)) {
            if (_settings.microDogLegs === 'error')
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.MicroDogLegs'));
            if (_settings.microDogLegs === 'warning') {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmesu.prompts.MicroDogLegsConfirm'),
                    () => { doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, true, false, undefined); },
                    () => { },
                    I18n.t('wmesu.common.Yes'),
                    I18n.t('wmesu.common.No')
                );
            }
        }
        microDogLegsContinue = true;
        if (segmentSelection.multipleConnectedComponents === false)
            endPointNodeIds = distinctNodes.filter(nodeId => !dupNodeIds.includes(nodeId));
        else
            endPointNodeIds = [selectedFeatures[0].model.attributes.fromNodeID, selectedFeatures[(selectedFeatures.length - 1)].model.attributes.toNodeID];
        const endPointNodeObjs = W.model.nodes.getByIds(endPointNodeIds),
            endPointNode1Geo = endPointNodeObjs[0].geometry.clone(),
            endPointNode2Geo = endPointNodeObjs[1].geometry.clone();
        if (getDeltaDirect(endPointNode1Geo.x, endPointNode2Geo.x) < 0) {
            let t = endPointNode1Geo.x;
            endPointNode1Geo.x = endPointNode2Geo.x;
            endPointNode2Geo.x = t;
            t = endPointNode1Geo.y;
            endPointNode1Geo.y = endPointNode2Geo.y;
            endPointNode2Geo.y = t;
            endPointNodeIds.push(endPointNodeIds[0]);
            endPointNodeIds.splice(0, 1);
            endPointNodeObjs.push(endPointNodeObjs[0]);
            endPointNodeObjs.splice(0, 1);
        }
        const a = endPointNode2Geo.y - endPointNode1Geo.y,
            b = endPointNode1Geo.x - endPointNode2Geo.x,
            c = endPointNode2Geo.x * endPointNode1Geo.y - endPointNode1Geo.x * endPointNode2Geo.y;
        distinctNodes.forEach(nodeId => {
            if (endPointNodeIds.indexOf(nodeId) === -1) {
                const node = W.model.nodes.getObjectById(nodeId),
                    nodeGeo = node.geometry.clone();
                const d = nodeGeo.y * a - nodeGeo.x * b,
                    r1 = getIntersectCoord(a, b, c, d);
                nodeGeo.x = r1.x;
                nodeGeo.y = r1.y;
                nodeGeo.calculateBounds();
                const connectedSegObjs = {};
                for (let idx = 0; idx < node.attributes.segIDs.length; idx++) {
                    const segId = node.attributes.segIDs[idx];
                    connectedSegObjs[segId] = W.model.segments.getObjectById(segId).geometry.clone();
                }
                const fromNodeLonLat = WazeWrap.Geometry.ConvertTo4326(node.geometry.x, node.geometry.y),
                    toNodeLonLat = WazeWrap.Geometry.ConvertTo4326(r1.x, r1.y);
                if (distanceBetweenPoints(fromNodeLonLat.lon, fromNodeLonLat.lat, toNodeLonLat.lon, toNodeLonLat.lat, 'meters') > 10)
                    longMove = true;
                nodesToMoveArr.push({
                    node, geometry: node.geometry, nodeGeo, connectedSegObjs
                });
            }
        });
        if (longMove && (_settings.longJnMove === 'error'))
            return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.LongJnMove'));
        if (longMove && (_settings.longJnMove === 'warning')) {
            return WazeWrap.Alerts.confirm(
                SCRIPT_NAME,
                I18n.t('wmesu.prompts.LongJnMoveConfirm'),
                () => {
                    doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, true, {
                        segmentsToRemoveGeometryArr, nodesToMoveArr, distinctNodes, endPointNodeIds
                    });
                },
                () => { },
                I18n.t('wmesu.common.Yes'),
                I18n.t('wmesu.common.No')
            );
        }
        doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, microDogLegsContinue, true, {
            segmentsToRemoveGeometryArr, nodesToMoveArr, distinctNodes, endPointNodeIds
        });
    } // W.selectionManager.selectedItems.length > 0
    else if (selectedFeatures.length === 1) {
        const seg = selectedFeatures[0],
            { model } = seg;
        if (model.type === 'segment') {
            if (!microDogLegsContinue && (checkForMicroDogLegs([model.attributes.fromNodeID, model.attributes.toNodeID], model.attributes.id) === true)) {
                if (_settings.microDogLegs === 'error')
                    return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmesu.error.MicroDogLegs'));
                if (_settings.microDogLegs === 'warning') {
                    return WazeWrap.Alerts.confirm(
                        SCRIPT_NAME,
                        I18n.t('wmesu.prompts.MicroDogLegsConfirm'),
                        () => { doStraightenSegments(sanityContinue, nonContinuousContinue, conflictingNamesContinue, true, false, undefined); },
                        () => { },
                        I18n.t('wmesu.common.Yes'),
                        I18n.t('wmesu.common.No')
                    );
                }
            }
            microDogLegsContinue = true;
            const newGeo = model.geometry.clone();
            // Remove the geometry nodes
            if (newGeo.components.length > 2) {
                const UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
                newGeo.components.splice(1, newGeo.components.length - 2);
                newGeo.components[0].calculateBounds();
                newGeo.components[1].calculateBounds();
                W.model.actionManager.add(new UpdateSegmentGeometry(model, model.geometry, newGeo));
                logDebug(`${I18n.t('wmesu.log.RemovedGeometryNodes')} # ${model.attributes.id}`);
            }
        }
    }
    else {
        logWarning(I18n.t('wmesu.log.NoSegmentsSelected'));
    }
    return true;
}

function insertSimplifyStreetGeometryButtons() {
    $('   <div id="WMESU-div-button" style="margin:0 0 10px 10px;">'
        + `     <button id="WME-SU" class="waze-btn waze-btn-small waze-btn-white" title="${I18n.t('wmesu.StraightenUpTitle')}">${I18n.t('wmesu.StraightenUp')}</button>`
        + ' </div>').insertAfter($('#edit-panel .segment .selection'));
}

function loadTranslations() {
    return new Promise(resolve => {
        const translations = {
                en: {
                    StraightenUp: 'Straighten up!',
                    StraightenUpTitle: 'Click here to straighten the selected segment(s) by removing geometry nodes and moving junction nodes as needed.',
                    common: {
                        From: 'from',
                        Help: 'Help',
                        No: 'No',
                        Note: 'Note',
                        NothingMajor: 'Nothing major.',
                        To: 'to',
                        Warning: 'Warning',
                        WhatsNew: 'What\'s new',
                        Yes: 'Yes'
                    },
                    error: {
                        ConflictingNames: 'You selected segments that do not share at least one name in common amongst all the segments and have the conflicting names setting set to error. '
                            + 'Segments not straightened.',
                        LongJnMove: 'One or more of the junction nodes that were to be moved would have been moved further than 10m and you have the long junction node move setting set to '
                            + 'give error. Segments not straightened.',
                        MicroDogLegs: 'One or more of the junctions nodes in the selection have a geonode within 2 meters. This is usually the sign of a micro dog leg (mDL).<br><br>'
                            + 'You have the setting for possibe micro doglegs set to give error. Segments not straightened.',
                        NonContinuous: 'You selected segments that are not all connected and have the non-continuous selected segments setting set to give error. Segments not straightened.',
                        TooManySegments: 'You selected too many segments and have the sanity check setting set to give error. Segments not straightened.'
                    },
                    help: {
                        Note01: 'This script uses the action manager, so changes can be undone before saving.',
                        Warning01: 'Enabling (Give warning, No warning) any of these settings can cause unexpected results. Use with caution!',
                        Step01: 'Select the starting segment.',
                        Step02: 'ALT+click the ending segment.',
                        Step02note: 'If the segments you wanted to straighten are not all selected, unselect them and start over using CTRL+click to select each segment instead.',
                        Step03: 'Click "Straighten up!" button in the sidebar.'
                    },
                    log: {
                        AllNodesStraight: 'All junction nodes that would be moved are already considered \'straight\'. No junction nodes were moved.',
                        EndPoints: 'End points',
                        MovingJunctionNode: 'Moving junction node',
                        NoSegmentsSelected: 'No segments selected.',
                        RemovedGeometryNodes: 'Removed geometry nodes for segment',
                        Segment: I18n.t('objects.segment.name'),
                        StraighteningSegments: 'Straightening segments'
                    },
                    prompts: {
                        ConflictingNamesConfirm: 'You selected segments that do not share at least one name in common amongst all the segments. Are you sure you wish to continue straightening?',
                        LongJnMoveConfirm: 'One or more of the junction nodes that are to be moved would be moved further than 10m. Are you sure you wish to continue straightening?',
                        MicroDogLegsConfirm: 'One or more of the junction nodes in the selection have a geonode within 2 meters. This is usually the sign of a micro dog leg (mDL).<br>'
                        + 'This geonode could exist on any segment connected to the junction nodes, not just the segments you selected.<br><br>'
                        + '<b>You should not continue until you are certain there are no micro dog legs.<b><br><br>'
                        + 'Are you sure you wish to continue straightening?',
                        NonContinuousConfirm: 'You selected segments that do not all connect. Are you sure you wish to continue straightening?',
                        SanityCheckConfirm: 'You selected many segments. Are you sure you wish to continue straightening?'
                    },
                    settings: {
                        GiveError: 'Give error',
                        GiveWarning: 'Give warning',
                        NoWarning: 'No warning',
                        ConflictingNames: 'Segments with conflicting names',
                        ConflictingNamesTitle: 'Select what to do if the selected segments do not share at least one name among their primary and alternate names (based on name, city and state).',
                        LongJnMove: 'Long junction node moves',
                        LongJnMoveTitle: 'Select what to do if one or more of the junction nodes would move further than 10m.',
                        MicroDogLegs: 'Possible micro doglegs (mDL)',
                        MicroDogLegsTitle: 'Select what to do if one or more of the junction nodes in the selection have a geometry node within 2m of itself, which is a possible micro dogleg (mDL).',
                        NonContinuous: 'Non-continuous selected segments',
                        NonContinuousTitle: 'Select what to do if the selected segments are not continuous.',
                        SanityCheck: 'Sanity check',
                        SanityCheckTitle: 'Select what to do if you selected a many segments.'
                    }
                },
                ru: {
                    StraightenUp: 'Выпрямить сегменты!',
                    StraightenUpTitle: 'Нажмите, чтобы выпрямить выбранные сегменты, удалив лишние геометрические точки и переместив узлы перекрёстков в ровную линию.',
                    common: {
                        From: 'с',
                        Help: 'Помощь',
                        No: 'Нет',
                        Note: 'Примечание',
                        NothingMajor: 'Не критично.',
                        To: 'до',
                        Warning: 'Предупреждение',
                        WhatsNew: 'Что нового',
                        Yes: 'Да'
                    },
                    error: {
                        ConflictingNames: 'Вы выбрали сегменты, которые не имеют хотя бы одного общего названия улицы среди выделенных.'
                            + 'Сегменты не были выпрямлены.',
                        LongJnMove: 'Для выпрямления сегментов, их узлы должны быть перемещены более чем на 10 м, но в настройках у вас установлено ограничение перемещения на такое большое '
                            + 'расстояние. Сегменты не были выпрямлены.',
                        MicroDogLegs: 'Один или несколько узлов выбранных сегментов имеют точку в пределах 2 метров. Обычно это признак “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”.<br><br>'
                            + 'В настройках для возможных микроискривлений у вас выставлено ограничение, чтобы выдать ошибку. Сегменты не были выпрямлены.',
                        NonContinuous: 'Вы выбрали сегменты, которые не соединены между собой, но в настройках у вас установлено ограничение для работы с такими сегментами. Сегменты не были '
                            + 'выпрямлены.',
                        TooManySegments: 'Вы выбрали слишком много сегментов, но в настройках у вас включено ограничение на количество одновременно обрабатываемых сегментов. Сегменты не были '
                            + 'выпрямлены.'
                    },
                    help: {
                        Note01: 'Этот скрипт использует историю действий, поэтому перед их сохранением изменения можно отменить.',
                        Warning01: 'Настройка любого из этих параметров в положение (Выдать предупреждение, Не предупреждать) может привести к неожиданным результатам. Используйте с осторожностью!',
                        Step01: 'Выделите начальный сегмент.',
                        Step02: 'При помощи Alt-кнопки, выделите конечный сегмент.',
                        Step02note: 'Если выделены не все нужные вам сегменты, при помощи Ctrl-кнопки можно дополнительно выделить или снять выделения сегментов.',
                        Step03: 'Нажмите ‘Выпрямить сегменты!’ на левой панели.'
                    },
                    log: {
                        AllNodesStraight: 'Все узлы, которые нужно было выпрямить, уже выровнены в линию. Сегменты оставлены без изменений.',
                        EndPoints: 'конечные точки',
                        MovingJunctionNode: 'Перемещение узла',
                        NoSegmentsSelected: 'Сегменты не выделены.',
                        RemovedGeometryNodes: 'Удалены лишние точки сегмента',
                        Segment: I18n.t('objects.segment.name'),
                        StraighteningSegments: 'Выпрямление сегментов'
                    },
                    prompts: {
                        ConflictingNamesConfirm: 'Вы выбрали сегменты, которые не имеют хотя бы одного общего названия среди всех сегментов. Вы уверены, что хотите продолжить выпрямление?',
                        LongJnMoveConfirm: 'Один или несколько узлов будут перемещены более, чем на 10 метров. Вы уверены, что хотите продолжить выпрямление?',
                        MicroDogLegsConfirm: 'Один или несколько узлов выбранных сегментов имеют точки в пределах 2 метров. Обычно это признак “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”.<br>'
                        + 'Такая точка может находиться в любом сегменте, соединенном с выбранными вами сегментами и узлами, а не только на них самих.<br><br>'
                        + '<b>Вы не должны продолжать до тех пор, пока не убедитесь, что у вас нет “микроискривлений”.<b><br><br>'
                        + 'Вы уверены,что готовы продолжать выпрямление?',
                        NonContinuousConfirm: 'Вы выбрали сегменты, которые не соединяются друг с другом. Вы уверены, что хотите продолжить выпрямление?',
                        SanityCheckConfirm: 'Вы выбрали слишком много сегментов. Вы уверены, что хотите продолжить выпрямление?'
                    },
                    settings: {
                        GiveError: 'Выдать ошибку',
                        GiveWarning: 'Выдать предупреждение',
                        NoWarning: 'Не предупреждать',
                        ConflictingNames: 'Сегменты с разными названиями',
                        ConflictingNamesTitle: 'Выберите, что делать, если выбранные сегменты не содержат хотя бы одно название среди своих основных и альтернативных названий (на основе улицы, '
                            + 'города и района).',
                        LongJnMove: 'Перемещение узлов на большие расстояния',
                        LongJnMoveTitle: 'Выберите, что делать, если один или несколько узлов будут перемещаться дальше, чем на 10 метров.',
                        MicroDogLegs: 'Допускать “<a href=”https://wazeopedia.waze.com/wiki/Benelux/Junction_Arrows” target=”blank”>микроискривления</a>”',
                        MicroDogLegsTitle: 'Выберите, что делать, если один или несколько узлов соединения в выделении имеют точку в пределах 2 м от себя, что является возможным “микроискривлением”.',
                        NonContinuous: 'Не соединённые сегменты',
                        NonContinuousTitle: 'Выберите, что делать, если выбранные сегменты не соединены друг с другом.',
                        SanityCheck: 'Ограничение нагрузки',
                        SanityCheckTitle: 'Выберите, что делать, если вы выбрали слишком много сегментов.'
                    }
                }
            },
            locale = I18n.currentLocale();
        I18n.translations[locale].wmesu = translations.en;
        translations['en-US'] = { ...translations.en };
        I18n.translations[locale].wmesu = $.extend({}, translations.en, translations[locale]);
        resolve();
    });
}

function registerEvents() {
    $('select[id^="WMESU-"]').off().on('change', function () {
        const setting = this.id.substr(6);
        if (this.value.toLowerCase() !== _settings[setting]) {
            _settings[setting] = this.value.toLowerCase();
            saveSettingsToStorage();
        }
    });
}

function buildSelections(selected) {
    const rVal = `<option value="nowarning"${(selected === 'nowarning' ? ' selected' : '')}>${I18n.t('wmesu.settings.NoWarning')}</option>`
    + `<option value="warning"${(selected === 'warning' ? ' selected' : '')}>${I18n.t('wmesu.settings.GiveWarning')}</option>`
    + `<option value="error"${(selected === 'error' ? ' selected' : '')}>${I18n.t('wmesu.settings.GiveError')}</option>`;
    return rVal;
}

async function init() {
    log('Initializing.');
    if (W.loginManager.getUserRank() < 2)
        return;
    await loadSettingsFromStorage();
    await loadTranslations();
    const $suTab = $('<div>', { style: 'padding:8px 16px', id: 'WMESUSettings' });
    $suTab.html([
        `<div style="margin-bottom:0px;font-size:13px;font-weight:600;">${SCRIPT_NAME}</div>`,
        `<div style="margin-top:0px;font-size:11px;font-weight:600;color:#aaa">${SCRIPT_VERSION}</div>`,
        `<div id="WMESU-div-conflictingNames" class="controls-container"><select id="WMESU-conflictingNames" style="font-size:11px;height:22px;" title="${I18n.t('wmesu.settings.ConflictingNamesTitle')}">`,
        buildSelections(_settings.conflictingNames),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmesu.settings.ConflictingNames')}</div>`,
        '</div><br/>',
        `<div id="WMESU-div-longJnMove" class="controls-container"><select id="WMESU-longJnMove" style="font-size:11px;height:22px;" title="${I18n.t('wmesu.settings.LongJnMoveTitle')}">`,
        buildSelections(_settings.longJnMove),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmesu.settings.LongJnMove')}</div>`,
        '</div><br/>',
        `<div id="WMESU-div-microDogLegs" class="controls-container"><select id="WMESU-microDogLegs" style="font-size:11px;height:22px;" title="${I18n.t('wmesu.settings.MicroDogLegsTitle')}">`,
        buildSelections(_settings.microDogLegs),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmesu.settings.MicroDogLegs')}</div>`,
        '</div><br/>',
        `<div id="WMESU-div-nonContinuousSelection" class="controls-container"><select id="WMESU-nonContinuousSelection" style="font-size:11px;height:22px;" title="${I18n.t('wmesu.settings.NonContinuousTitle')}">`,
        buildSelections(_settings.nonContinuousSelection),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmesu.settings.NonContinuous')}</div>`,
        '</div><br/>',
        `<div id="WMESU-div-sanityCheck" class="controls-container"><select id="WMESU-sanityCheck" style="font-size:11px;height:22px;" title="${I18n.t('wmesu.settings.SanityCheckTitle')}">`,
        buildSelections(_settings.sanityCheck),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmesu.settings.SanityCheck')}</div>`,
        `<div style="margin-top:20px;"><div style="font-size:14px;font-weight:600;">${I18n.t('wmesu.common.Help')}:</div><div><ol style="font-weight:600;">`,
        `<li><p style="font-weight:100;margin-bottom:0px;">${I18n.t('wmesu.help.Step01')}</p></li>`,
        `<li><p style="font-weight:100;margin-bottom:0px;">${I18n.t('wmesu.help.Step02')}<br><b>${I18n.t('wmesu.common.Note')}:</b> ${I18n.t('wmesu.help.Step02note')}</p></li>`,
        `<li><p style="font-weight:100;margin-bottom:0px;">${I18n.t('wmesu.help.Step03')}</p></li></ol></div>`,
        `<b>${I18n.t('wmesu.common.Warning')}:</b> ${I18n.t('wmesu.help.Warning01')}<br><br><b>${I18n.t('wmesu.common.Note')}:</b> ${I18n.t('wmesu.help.Note01')}</div></div>`
    ].join(' '));
    new WazeWrap.Interface.Tab('SU!', $suTab.html(), registerEvents);
    logDebug('Enabling MOs.');
    _editPanelObserver.observe(document.querySelector('#edit-panel > div'), {
        childList: true, attributes: false, attributeOldValue: false, characterData: false, characterDataOldValue: false, subtree: true
    });
    if (W.selectionManager.getSegmentSelection().segments.length > 0)
        insertSimplifyStreetGeometryButtons();
    $('#sidebar').on('click', '#WME-SU', e => {
        e.preventDefault();
        doStraightenSegments();
    });
    window.addEventListener('beforeunload', () => { checkShortcutChanged(); }, false);
    new WazeWrap.Interface.Shortcut(
        'runStraightenUpShortcut',
        'Run straighten up',
        'editing',
        'Straighten Up',
        _settings.runStraightenUpShortcut,
        () => {
            if ($('#WME-SU').length > 0)
                $('#WME-SU').click();
        },
        null
    ).add();
    showScriptInfoAlert();
    log(`Fully initialized in ${Math.round(performance.now() - LOAD_BEGIN_TIME)} ms.`);
    setTimeout(checkShortcutChanged, 10000);
}

function bootstrap(tries) {
    if (W && W.map && W.model && $ && WazeWrap.Ready && require) {
        checkTimeout({ timeout: 'bootstrap' });
        log('Bootstrapping.');
        init();
    }
    else if (tries < 1000) {
        logDebug(`Bootstrap failed. Retrying ${tries} of 1000`);
        _timeouts.bootstrap = window.setTimeout(bootstrap, 200, ++tries);
    }
    else {
        logError('Bootstrap timed out waiting for WME to become ready.');
    }
}

bootstrap(1);