WME Straighten Up!

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        WME Straighten Up!
// @namespace   https://greasyfork.org/users/166843
// @version     2024.01.31.01
// @description Straighten selected WME segment(s) by aligning along straight line between two end points and removing geometry nodes.
// @author      dBsooner
// @match       http*://*.waze.com/*editor*
// @exclude     http*://*.waze.com/user/editor*
// @require     https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant       GM_xmlhttpRequest
// @connect     greasyfork.org
// @license     GPLv3
// ==/UserScript==

// Original credit to jonny3D and impulse200

/* global I18n, GM_info, GM_xmlhttpRequest, W, WazeWrap */

(function () {
    'use strict';

    // eslint-disable-next-line no-nested-ternary
    const _SCRIPT_SHORT_NAME = `WME SU!${(/beta/.test(GM_info.script.name) ? ' β' : /\(DEV\)/i.test(GM_info.script.name) ? ' Ω' : '')}`,
        _SCRIPT_LONG_NAME = GM_info.script.name,
        _IS_ALPHA_VERSION = /[Ω]/.test(_SCRIPT_SHORT_NAME),
        _IS_BETA_VERSION = /[β]/.test(_SCRIPT_SHORT_NAME),
        // SCRIPT_AUTHOR = GM_info.script.author,
        _PROD_DL_URL = 'https://greasyfork.org/scripts/388349-wme-straighten-up/code/WME%20Straighten%20Up!.user.js',
        _FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=289116',
        _SETTINGS_STORE_NAME = 'WMESU',
        _BETA_DL_URL = 'YUhSMGNITTZMeTluY21WaGMzbG1iM0pyTG05eVp5OXpZM0pwY0hSekx6TTRPRE0xTUMxM2JXVXRjM1J5WVdsbmFIUmxiaTExY0MxaVpYUmhMMk52WkdVdlYwMUZKVEl3VTNSeVlXbG5hSFJsYmlVeU1GVndJU1V5TUNoaVpYUmhLUzUxYzJWeUxtcHo=',
        _ALERT_UPDATE = true,
        _SCRIPT_VERSION = GM_info.script.version.toString(),
        _SCRIPT_VERSION_CHANGES = ['BUGFIX: Check for micro dog leg (mDL)'],
        _DEBUG = /[βΩ]/.test(_SCRIPT_SHORT_NAME),
        _LOAD_BEGIN_TIME = performance.now(),
        _elems = {
            b: document.createElement('b'),
            br: document.createElement('br'),
            div: document.createElement('div'),
            li: document.createElement('li'),
            ol: document.createElement('ol'),
            option: document.createElement('option'),
            p: document.createElement('p'),
            select: document.createElement('select'),
            'wz-button': document.createElement('wz-button'),
            'wz-card': document.createElement('wz-card')
        },
        _timeouts = { onWmeReady: undefined, saveSettingsToStorage: undefined };
    let _settings = {};

    function log(message, data = '') { console.log(`${_SCRIPT_SHORT_NAME}:`, message, data); }
    function logError(message, data = '') { console.error(`${_SCRIPT_SHORT_NAME}:`, new Error(message), data); }
    function logWarning(message, data = '') { console.warn(`${_SCRIPT_SHORT_NAME}:`, message, data); }
    function logDebug(message, data = '') {
        if (_DEBUG)
            log(message, data);
    }

    function $extend(...args) {
        const extended = {},
            deep = Object.prototype.toString.call(args[0]) === '[object Boolean]' ? args[0] : false,
            merge = function (obj) {
                Object.keys(obj).forEach((prop) => {
                    if (Object.prototype.hasOwnProperty.call(obj, prop)) {
                        if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]')
                            extended[prop] = $extend(true, extended[prop], obj[prop]);
                        else if ((obj[prop] !== undefined) && (obj[prop] !== null))
                            extended[prop] = obj[prop];
                    }
                });
            };
        for (let i = deep ? 1 : 0, { length } = args; i < length; i++) {
            if (args[i])
                merge(args[i]);
        }
        return extended;
    }

    function createElem(type = '', attrs = {}, eventListener = []) {
        const el = _elems[type]?.cloneNode(false) || _elems.div.cloneNode(false),
            applyEventListeners = function ([evt, cb]) {
                return this.addEventListener(evt, cb);
            };
        Object.keys(attrs).forEach((attr) => {
            if ((attrs[attr] !== undefined) && (attrs[attr] !== 'undefined') && (attrs[attr] !== null) && (attrs[attr] !== 'null')) {
                if ((attr === 'disabled') || (attr === 'checked') || (attr === 'selected') || (attr === 'textContent') || (attr === 'innerHTML'))
                    el[attr] = attrs[attr];
                else
                    el.setAttribute(attr, attrs[attr]);
            }
        });
        if (eventListener.length > 0) {
            eventListener.forEach((obj) => {
                Object.entries(obj).map(applyEventListeners.bind(el));
            });
        }
        return el;
    }

    function createTextNode(str = '') {
        return document.createTextNode(str);
    }

    function dec(s = '') {
        return atob(atob(s));
    }

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

    async function loadSettingsFromStorage() {
        const defaultSettings = {
                conflictingNames: 'warning',
                longJnMove: 'warning',
                microDogLegs: 'warning',
                nonContinuousSelection: 'warning',
                sanityCheck: 'warning',
                runStraightenUpShortcut: '',
                lastSaved: 0,
                lastVersion: undefined
            },
            loadedSettings = JSON.parse(localStorage.getItem(_SETTINGS_STORE_NAME));
        _settings = $extend(true, {}, defaultSettings, loadedSettings);
        const serverSettings = await WazeWrap.Remote.RetrieveSettings(_SETTINGS_STORE_NAME);
        if (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)) {
            const divElemRoot = createElem('div');
            divElemRoot.appendChild(createElem('p', { textContent: 'What\'s New:' }));
            const ulElem = createElem('ul');
            if (_SCRIPT_VERSION_CHANGES.length > 0) {
                for (let idx = 0, { length } = _SCRIPT_VERSION_CHANGES; idx < length; idx++)
                    ulElem.appendChild(createElem('li', { innerHTML: _SCRIPT_VERSION_CHANGES[idx] }));
            }
            else {
                ulElem.appendChild(createElem('li', { textContent: 'Nothing major.' }));
            }
            divElemRoot.appendChild(ulElem);
            WazeWrap.Interface.ShowScriptUpdate(_SCRIPT_SHORT_NAME, _SCRIPT_VERSION, divElemRoot.innerHTML, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL).replace(/code\/.*\.js/, ''), _FORUM_URL);
        }
    }

    // рассчитаем пересчечение перпендикуляра точки с наклонной прямой
    // 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];
        // eslint-disable-next-line no-mixed-operators
        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(segmentSelectionArr = []) {
        const streetIds = [],
            streetIdsForEach = (streetId) => { streetIds.push(streetId); };
        for (let idx = 0, { length } = segmentSelectionArr; idx < length; idx++) {
            if (idx > 0) {
                if ((segmentSelectionArr[idx].getPrimaryStreetID() > 0) && streetIds.includes(segmentSelectionArr[idx].getPrimaryStreetID()))
                // eslint-disable-next-line no-continue
                    continue;
                if (segmentSelectionArr[idx].getAttribute('streetIDs').length > 0) {
                    let included = false;
                    for (let idx2 = 0, len = segmentSelectionArr[idx].getAttribute('streetIDs').length; idx2 < len; idx2++) {
                        included = streetIds.includes(segmentSelectionArr[idx].getAttribute('streetIDs')[idx2]);
                        if (included)
                            break;
                    }
                    if (included === true)
                    // eslint-disable-next-line no-continue
                        continue;
                    else
                        return false;
                }
                return false;
            }
            if (idx === 0) {
                if (segmentSelectionArr[idx].getPrimaryStreetID() > 0)
                    streetIds.push(segmentSelectionArr[idx].getPrimaryStreetID());
                if (segmentSelectionArr[idx].getAttribute('streetIDs').length > 0)
                    segmentSelectionArr[idx].getAttribute('streetIDs').forEach(streetIdsForEach);
            }
        }
        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;
        const R = 6371; // KM
        const φ1 = lat1 * (Math.PI / 180);
        const φ2 = lat2 * (Math.PI / 180);
        const Δφ = (lat2 - lat1) * (Math.PI / 180);
        const Δλ = (lon2 - lon1) * (Math.PI / 180);
        const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        const d = R * c;
        return d * 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 = function (geoComp) {
            const testNode4326 = { lon: geoComp[0], lat: geoComp[1] };
            if ((this.lon !== testNode4326.lon) || (this.lat !== testNode4326.lat)) {
                if (distanceBetweenPoints(this.lon, this.lat, testNode4326.lon, testNode4326.lat, 'meters') < 2)
                    return false;
            }
            return true;
        };
        for (let idx = 0, { length } = nodesObjArr; idx < length; idx++) {
            if (!nodesChecked.includes(nodesObjArr[idx])) {
                nodesChecked.push(nodesObjArr[idx]);
                const segmentsObjArr = W.model.segments.getByIds(nodesObjArr[idx].getSegmentIds()) || [],
                    node4326 = {
                        lon: nodesObjArr[idx].getGeometry().coordinates[0],
                        lat: nodesObjArr[idx].getGeometry().coordinates[1]
                    };
                for (let idx2 = 0, len = segmentsObjArr.length; idx2 < len; idx2++) {
                    const segObj = segmentsObjArr[idx2];
                    if (!singleSegmentId
                    || (singleSegmentId && (segObj.getID() === singleSegmentId))) {
                        if (!segObj.getGeometry().coordinates.every(checkGeoComp.bind(node4326)))
                            return true;
                    }
                }
            }
        }
        return false;
    }

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

    function insertSimplifyStreetGeometryButtons() {
        const wmeSuDiv = document.getElementById('WME-SU-div'),
            elem = document.getElementById('segment-edit-general');
        if (!elem)
            return;
        const docFrags = document.createDocumentFragment();
        if (!wmeSuDiv) {
            const contentDiv = createElem('div', { style: 'align-items:center; cursor:pointer; display:flex; font-size:13px; gap:8px; justify-content:flex-start;', textContent: I18n.t('wmesu.StraightenUp') });
            contentDiv.appendChild(createElem('wz-button', {
                id: 'WME-SU', color: 'secondary', size: 'xs', textContent: I18n.t('wmesu.common.DoIt'), title: I18n.t('wmesu.StraightenUpTitle')
            }, [{ click: doStraightenSegments }]));
            const wzCard = createElem('wz-card', { style: '--wz-card-padding:4px 8px; --wz-card-margin:0; --wz-card-width:auto; display:block; margin-bottom:8px;' });
            wzCard.appendChild(contentDiv);
            const divElemRoot = createElem('div', { id: 'WME-SU-div' });
            divElemRoot.appendChild(wzCard);
            docFrags.appendChild(divElemRoot);
        }
        if (docFrags.firstChild)
            elem.insertBefore(docFrags, elem.firstChild);
    }

    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: {
                            DoIt: 'Do It',
                            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).',
                            NonContinuousSelection: 'Non-continuous selected segments',
                            NonContinuousSelectionTitle: '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: {
                            DoIt: 'Сделай это',
                            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(true, {}, translations.en, translations[locale]);
            resolve();
        });
    }

    function checkSuVersion() {
        if (_IS_ALPHA_VERSION)
            return;
        let updateMonitor;
        try {
            updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(_SCRIPT_LONG_NAME, _SCRIPT_VERSION, (_IS_BETA_VERSION ? dec(_BETA_DL_URL) : _PROD_DL_URL), GM_xmlhttpRequest);
            updateMonitor.start();
        }
        catch (err) {
            logError('Upgrade version check:', err);
        }
    }

    async function onWazeWrapReady() {
        log('Initializing.');
        checkSuVersion();
        if (W.loginManager.getUserRank() < 2)
            return;
        await loadSettingsFromStorage();
        await loadTranslations();
        const onSelectionChange = function () {
                const setting = this.id.substr(6);
                if (this.value.toLowerCase() !== _settings[setting]) {
                    _settings[setting] = this.value.toLowerCase();
                    saveSettingsToStorage();
                }
            },
            buildSelections = (selected) => {
                const docFrags = document.createDocumentFragment();
                docFrags.appendChild(createElem('option', { value: 'nowarning', selected: selected === 'nowarning', textContent: I18n.t('wmesu.settings.NoWarning') }));
                docFrags.appendChild(createElem('option', { value: 'warning', selected: selected === 'warning', textContent: I18n.t('wmesu.settings.GiveWarning') }));
                docFrags.appendChild(createElem('option', { value: 'error', selected: selected === 'error', textContent: I18n.t('wmesu.settings.GiveError') }));
                return docFrags;
            },
            buildSection = (section) => {
                const selectElem = createElem('select', {
                    id: `WMESU-${section}`,
                    style: 'font-size:11px;height:22px;',
                    title: I18n.t(`wmesu.settings.${section.charAt(0).toUpperCase()}${section.slice(1)}Title`)
                }, [{ change: onSelectionChange }]);
                selectElem.appendChild(buildSelections(_settings[section]));
                const divElemDiv = createElem('div', { id: `WMESU-div-${section}`, class: 'controls-container' });
                divElemDiv.appendChild(selectElem);
                const divElemDivDiv = createElem('div', { style: 'display:inline-block;font-size:11px;', textContent: I18n.t(`wmesu.settings.${section.charAt(0).toUpperCase()}${section.slice(1)}`) });
                divElemDiv.appendChild(divElemDivDiv);
                return divElemDiv;
            },
            tabContent = () => {
                const docFrags = document.createDocumentFragment();
                docFrags.appendChild(createElem('div', { style: 'margin-bottom:0px;font-size:13px;font-weight:600;', textContent: _SCRIPT_SHORT_NAME }));
                docFrags.appendChild(createElem('div', { style: 'margin-top:0px;font-size:11px;font-weight:600;color:#aaa;', textContent: _SCRIPT_VERSION }));
                docFrags.appendChild(buildSection('conflictingNames'));
                docFrags.appendChild(buildSection('longJnMove'));
                docFrags.appendChild(buildSection('microDogLegs'));
                docFrags.appendChild(buildSection('nonContinuousSelection'));
                docFrags.appendChild(buildSection('sanityCheck'));
                const divElemDiv = createElem('div', { style: 'margin-top:20px;' });
                divElemDiv.appendChild(createElem('div', { style: 'font-size:14px;font-weight:600;', textContent: I18n.t('wmesu.common.Help') }));
                let liElem = createElem('li');
                liElem.appendChild(createElem('p', { style: 'font-weight:100;margin-bottom:0px;', textContent: I18n.t('wmesu.help.Step01') }));
                const olElem = createElem('ol', { style: 'font-weight:600;' });
                olElem.appendChild(liElem);
                const pElem = createElem('p', { style: 'font-weight:100;margin-bottom:0px;' });
                pElem.appendChild(createTextNode(I18n.t('wmesu.help.Step02')));
                pElem.appendChild(createElem('br'));
                pElem.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Note')}:` }));
                pElem.appendChild(createTextNode(` ${I18n.t('wmesu.help.Step02note')}`));
                liElem = createElem('li');
                liElem.appendChild(pElem);
                olElem.appendChild(liElem);
                liElem = createElem('li');
                liElem.appendChild(createElem('p', { style: 'font-weight:100;margin-bottom:0px;', textContent: I18n.t('wmesu.help.Step03') }));
                olElem.appendChild(liElem);
                const divElemDivDiv = createElem('div');
                divElemDivDiv.appendChild(olElem);
                divElemDiv.appendChild(divElemDivDiv);
                divElemDiv.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Warning')}:` }));
                divElemDiv.appendChild(createTextNode(` ${I18n.t('wmesu.help.Warning01')}`));
                divElemDiv.appendChild(createElem('br'));
                divElemDiv.appendChild(createElem('br'));
                divElemDiv.appendChild(createElem('b', { textContent: `${I18n.t('wmesu.common.Note')}:` }));
                divElemDiv.appendChild(createTextNode(` ${I18n.t('wmesu.help.Note01')}`));
                docFrags.appendChild(divElemDiv);
                return docFrags;
            };
        const { tabLabel, tabPane } = W.userscripts.registerSidebarTab('SU!');
        tabLabel.textContent = 'SU!';
        tabLabel.title = _SCRIPT_LONG_NAME;
        tabPane.appendChild(tabContent());
        tabPane.id = 'WMESUSettings';
        await W.userscripts.waitForElementConnected(tabPane);
        logDebug('Enabling MOs.');
        W.selectionManager.events.register('selectionchanged', null, insertSimplifyStreetGeometryButtons);
        if (W.selectionManager.getSegmentSelection().segments.length > 0)
            insertSimplifyStreetGeometryButtons();
        window.addEventListener('beforeunload', () => { checkShortcutChanged(); }, false);
        new WazeWrap.Interface.Shortcut(
            'runStraightenUpShortcut',
            'Run straighten up',
            'editing',
            'Straighten Up',
            _settings.runStraightenUpShortcut,
            () => document.getElementById('WME-SU')?.dispatchEvent(new MouseEvent('click', { bubbles: true })),
            null
        ).add();
        showScriptInfoAlert();
        log(`Fully initialized in ${Math.round(performance.now() - _LOAD_BEGIN_TIME)} ms.`);
        setTimeout(checkShortcutChanged, 10000);
    }

    function onWmeReady(tries = 1) {
        if (typeof tries === 'object')
            tries = 1;
        checkTimeout({ timeout: 'onWmeReady' });
        if (WazeWrap?.Ready) {
            logDebug('WazeWrap is ready. Proceeding with initialization.');
            onWazeWrapReady();
        }
        else if (tries < 1000) {
            logDebug(`WazeWrap is not in Ready state. Retrying ${tries} of 1000.`);
            _timeouts.onWmeReady = window.setTimeout(onWmeReady, 200, ++tries);
        }
        else {
            logError('onWmeReady timed out waiting for WazeWrap Ready state.');
        }
    }

    function onWmeInitialized() {
        if (W.userscripts?.state?.isReady) {
            logDebug('W is ready and already in "wme-ready" state. Proceeding with initialization.');
            onWmeReady();
        }
        else {
            logDebug('W is ready, but state is not "wme-ready". Adding event listener.');
            document.addEventListener('wme-ready', onWmeReady, { once: true });
        }
    }

    function bootstrap() {
        if (!W) {
            logDebug('W is not available. Adding event listener.');
            document.addEventListener('wme-initialized', onWmeInitialized, { once: true });
        }
        else {
            onWmeInitialized();
        }
    }

    bootstrap();
}
)();