WME Simplify Street Geometry

Flatten selected segments into a perfectly straight line.

当前为 2019-08-09 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME Simplify Street Geometry
// @namespace   https://greasyfork.org/users/166843
// @version      2019.08.09.01
// @description  Flatten selected segments into a perfectly straight line.
// @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 localStorage, window, $, 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 = '',
    SCRIPT_GF_URL = 'https://greasyfork.org/en/scripts/388349-wme-simplify-street-geometry',
    SCRIPT_NAME = GM_info.script.name.replace('(beta)', 'β'),
    SCRIPT_VERSION = GM_info.script.version,
    SCRIPT_VERSION_CHANGES = ['<b>NEW:</b> Initial release.'],
    SETTINGS_STORE_NAME = 'WMESSG',
    _timeouts = { bootstrap: undefined };
let _moveNode,
    _settings = {},
    _updateSegmentGeometry;

function loadSettingsFromStorage() {
    return new Promise(async resolve => {
        const defaultSettings = {
                conflictingNames: 'warning',
                nonContinuousSelection: 'warning',
                sanityCheck: 'warning',
                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);
        resolve();
    });
}

function 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 showScriptInfoAlert() {
    if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
        let releaseNotes = '';
        releaseNotes += `<p>${I18n.t('wmessg.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('wmessg.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('SSG:', message); }
function logError(message) { console.error('SSG:', message); }
function logWarning(message) { console.warn('SSG:', message); }
function logDebug(message) {
    if (DEBUG)
        console.log('SSG:', 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 doSimplifyStreetGeometry(sanityContinue, nonContinuousContinue, conflictingNamesContinue) {
    const numSelectedFeatures = W.selectionManager.getSelectedFeatures().length;
    if (numSelectedFeatures > 1) {
        const selectedFeatures = W.selectionManager.getSelectedFeatures();
        if ((numSelectedFeatures > 10) && !sanityContinue) {
            if (_settings.sanityCheck === 'error')
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmessg.error.TooManySegments'));
            if (_settings.sanityCheck === 'warning') {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmessg.prompts.SanityCheckConfirm'),
                    () => { doSimplifyStreetGeometry(true); },
                    () => { },
                    I18n.t('wmessg.common.Yes'),
                    I18n.t('wmessg.common.No')
                );
            }
            sanityContinue = true;
        }
        if ((W.selectionManager.getSegmentSelection().multipleConnectedComponents === true) && !nonContinuousContinue) {
            if (_settings.nonContinuousSelection === 'error')
                return WazeWrap.Alerts.error(SCRIPT_NAME, I18n.t('wmessg.error.NonContinuous'));
            if (_settings.nonContinuousSelection === 'warning') {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmessg.prompts.NonContinuousConfirm'),
                    () => { doSimplifyStreetGeometry(sanityContinue, true); },
                    () => { },
                    I18n.t('wmessg.common.Yes'),
                    I18n.t('wmessg.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('wmessg.error.ConflictingNames'));
            if (!continuousNames && !conflictingNamesContinue && (_settings.conflictingNames === 'warning')) {
                return WazeWrap.Alerts.confirm(
                    SCRIPT_NAME,
                    I18n.t('wmessg.prompts.ConflictingNamesConfirm'),
                    () => { doSimplifyStreetGeometry(sanityContinue, nonContinuousContinue, true); },
                    () => { },
                    I18n.t('wmessg.common.Yes'),
                    I18n.t('wmessg.common.No')
                );
            }
            conflictingNamesContinue = true;
        }
        let t1,
            t2,
            t,
            a = 0.0,
            b = 0.0,
            c = 0.0;
        // определим линию выравнивания
        // Define an alignment line
        logDebug(I18n.t('wmessg.log.CalculationOfInclinedLine'));
        for (let idx = 0; idx < numSelectedFeatures; idx++) {
            const seg = selectedFeatures[idx];
            if (seg.model.type === 'segment') {
                const geo = seg.model.geometry;
                // определяем формулу наклонной прямой
                // Determine the formula of the inclined line
                if (geo.components.length > 1) {
                    const a1 = geo.components[0].clone(),
                        a2 = geo.components[geo.components.length - 1].clone();
                    let dX = getDeltaDirect(a1.x, a2.x),
                        dY = getDeltaDirect(a1.y, a2.y);
                    const tX = idx > 0 ? getDeltaDirect(t1.x, t2.x) : 0,
                        tY = idx > 0 ? getDeltaDirect(t1.y, t2.y) : 0;
                    logDebug(`${I18n.t('wmessg.log.CalculatedLineVector')}: tX=${tX}, tY=${tY}`);
                    logDebug(`${I18n.t('wmessg.log.Segment')} #${(idx + 1)} (${a1.x}; ${a1.y}) - (${a2.x}; ${a2.y}), dX=${dX}, dY=${dY}`);
                    if (dX < 0) {
                        t = a1.x;
                        a1.x = a2.x;
                        a2.x = t;
                        t = a1.y;
                        a1.y = a2.y;
                        a2.y = t;
                        dX = getDeltaDirect(a1.x, a2.x);
                        dY = getDeltaDirect(a1.y, a2.y);
                        logDebug(`${I18n.t('wmessg.log.ExpandTheSegment')} #${(idx + 1)} (${a1.x}; ${a1.y}) - (${a2.x}; ${a2.y}), dX=${dX}, dY=${dY}`);
                    }
                    if (idx === 0) {
                        t1 = a1.clone();
                        t2 = a2.clone();
                    }
                    else {
                        if (a1.x < t1.x) {
                            t1.x = a1.x;
                            t1.y = a1.y;
                        }
                        if (a2.x > t2.x) {
                            t2.x = a2.x;
                            t2.y = a2.y;
                        }
                    }
                    logDebug(`${I18n.t('wmessg.log.SettlementDirectBy')} (${t1.x}; ${t1.y}) - (${t2.x}; ${t2.y})`);
                }
            }
            else {
                logWarning(I18n.t('wmessg.log.NonSegmentFound'));
            }
        }
        a = t2.y - t1.y;
        b = t1.x - t2.x;
        c = t2.x * t1.y - t1.x * t2.y;
        logDebug(I18n.t('wmessg.log.DirectAlignmentCalculated'));
        logDebug(`${I18n.t('wmessg.log.EndPoints')}: (${t1.x}; ${t1.y}) - (${t2.x}; ${t2.y})`);
        logDebug(`${I18n.t('wmessg.log.DirectFormula')}: ${a}x + ${b}y + ${c}`);
        logDebug(`${I18n.t('wmessg.log.AlignSegments')}... ${numSelectedFeatures}`);
        for (let idx = 0; idx < numSelectedFeatures; idx++) {
            const seg = selectedFeatures[idx],
                { model } = seg;
            if (model.type === 'segment') {
                const newGeo = model.geometry.clone();
                let flagSimpled = false;
                // удаляем лишние узлы
                // Remove the extra nodes
                if (newGeo.components.length > 2) {
                    newGeo.components.splice(1, newGeo.components.length - 2);
                    flagSimpled = true;
                }
                // упрощаем сегмент, если нужно
                // Simplify the segment, if necessary
                if (flagSimpled)
                    W.model.actionManager.add(new _updateSegmentGeometry(model, model.geometry, newGeo));
                    // работа с узлом
                    // Work with a node
                const node = W.model.nodes.getObjectById(model.attributes.fromNodeID),
                    nodeGeo = node.geometry.clone(),
                    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 = {},
                    emptyObj = {};
                for (let idx2 = 0; idx2 < node.attributes.segIDs.length; idx2++) {
                    const segId = node.attributes.segIDs[idx2];
                    connectedSegObjs[segId] = W.model.segments.getObjectById(segId).geometry.clone();
                }
                W.model.actionManager.add(new _moveNode(node, node.geometry, nodeGeo, connectedSegObjs, emptyObj));
                const node2 = W.model.nodes.getObjectById(model.attributes.toNodeID),
                    nodeGeo2 = node2.geometry.clone(),
                    d2 = nodeGeo2.y * a - nodeGeo2.x * b,
                    r2 = getIntersectCoord(a, b, c, d2);
                nodeGeo2.x = r2.x;
                nodeGeo2.y = r2.y;
                nodeGeo2.calculateBounds();
                for (let idx2 = 0; idx2 < node2.attributes.segIDs.length; idx2++) {
                    const segId = node2.attributes.segIDs[idx2];
                    connectedSegObjs[segId] = W.model.segments.getObjectById(segId).geometry.clone();
                }
                W.model.actionManager.add(new _moveNode(node2, node2.geometry, nodeGeo2, connectedSegObjs, emptyObj));
                logDebug(`${I18n.t('wmessg.log.Segment')} #${(idx + 1)} (${r1.x}; ${r1.y}) - (${r2.x}; ${r2.y})`);
            }
            else {
                logWarning(I18n.t('wmessg.log.NonSegmentFound'));
            }
        }
    } // W.selectionManager.selectedItems.length > 0
    else if (numSelectedFeatures === 1) {
        return WazeWrap.Alerts.info(SCRIPT_NAME, I18n.t('wmessg.error.OnlyOneSegment'));
    }
    else {
        logWarning(I18n.t('wmessg.log.NoSegmentsSelected'));
    }
    return true;
}

function insertSimplifyStreetGeometryButtons() {
    $('.edit-restrictions').after(`<button id="WME-SSG" class="waze-btn waze-btn-small waze-btn-white" title="${I18n.t('wmessg.SimplifyGeometryTitle')}">${I18n.t('wmessg.SimplifyGeometry')}</button>`);
}

function loadTranslations() {
    return new Promise(resolve => {
        const translations = {
                en: {
                    SimplifyGeometry: 'Simplify Geometry',
                    SimplifyGeometryTitle: 'Click here to flatten the selected segments into a straight line.',
                    common: {
                        No: 'No',
                        NothingMajor: 'Nothing major.',
                        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 simplified.',
                        NonContinuousSelection: 'You selected segments that are not all connected and have the non-continuous selected segments setting set to give error. Segments not simplified.',
                        OnlyOneSegment: 'You only selected one segment. This script is designed to work with more than one segment selected. Segments not simplified.',
                        TooManySegments: 'You selected too many segments and have the sanity check setting set to give error. Segments not simplified.'
                    },
                    log: {
                        AlignSegments: 'Align segments',
                        CalculatedLineVector: 'Calculated line vector',
                        CalculationOfInclinedLine: 'Calculation of the inclined line formula...',
                        DirectAlignmentCalculated: 'Direct alignment calculated.',
                        DirectFormula: 'Direct formula',
                        EndPoints: 'End points',
                        ExpandTheSegment: 'Expand the segment',
                        NonSegmentFound: 'Non segment found in selection.',
                        NoSegmentsSelected: 'No segments selected.',
                        Segment: I18n.t('objects.segment.name'),
                        SettlementDirectBy: 'Settlement direct by'
                    },
                    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 simplifaction?',
                        NonContinuousConfirm: 'You selected segments that do not all connect. Are you sure you wish to continue with simplification?',
                        SanityCheckConfirm: 'You selected many segments. Are you sure you wish to continue with simplification?'
                    },
                    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 all have the same name.',
                        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: {
                    SimplifyGeometry: 'Выровнять улицу',
                    log: {
                        AlignSegments: 'выравниваем сегменты',
                        CalculatedLineVector: 'расчётный вектор линии',
                        CalculationOfInclinedLine: 'расчёт формулы наклонной прямой...',
                        DirectAlignmentCalculated: 'прямая выравнивания рассчитана.',
                        DirectFormula: 'формула прямой',
                        EndPoints: 'конечные точки',
                        ExpandTheSegment: 'разворачиваем сегмент',
                        Segment: I18n.t('objects.segment.name'),
                        SettlementDirectBy: 'расчётная прямая по'
                    }
                }
            },
            locale = I18n.currentLocale(),
            availTranslations = Object.keys(translations);
        I18n.translations[locale].wmessg = translations.en;
        if (availTranslations.indexOf(I18n.currentLocale()) > 0) {
            Object.keys(translations[locale]).forEach(prop => {
                if (typeof translations[locale][prop] === 'object') {
                    Object.keys(translations[locale][prop]).forEach(subProp => {
                        if (translations[locale][prop][subProp] !== '')
                            I18n.translations[locale].wmessg[prop][subProp] = translations[locale][prop][subProp];
                    });
                }
                else if (translations[locale][prop] !== '') {
                    I18n.translations[locale].wmessg[prop] = translations[locale][prop];
                }
            });
        }
        resolve();
    });
}

function registerEvents() {
    $('#WMESSG-conflictingNames').off().on('change', function () {
        const setting = this.id.substr(7);
        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('wmessg.settings.NoWarning')}</option>`
    + `<option value="warning"${(selected === 'warning' ? ' selected' : '')}>${I18n.t('wmessg.settings.GiveWarning')}</option>`
    + `<option value="error"${(selected === 'error' ? ' selected' : '')}>${I18n.t('wmessg.settings.GiveError')}</option>`;
    return rVal;
}

async function init() {
    log('Initializing.');
    await loadSettingsFromStorage();
    await loadTranslations();
    const $ssgTab = $('<div>', { style: 'padding:8px 16px', id: 'WMESSGSettings' });
    $ssgTab.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="WMESSG-div-conflictingNames" class="controls-container"><select id="WMESSG-conflictingNames" style="font-size:11px;height:22px;" title="${I18n.t('wmessg.settings.ConflictingNamesTitle')}">`,
        buildSelections(_settings.conflictingNames),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmessg.settings.ConflictingNames')}</div>`,
        '</div><br/>',
        `<div id="WMESSG-div-nonContinuousSelection" class="controls-container"><select id="WMESSG-nonContinuousSelection" style="font-size:11px;height:22px;" title="${I18n.t('wmessg.settings.NonContinuousTitle')}">`,
        buildSelections(_settings.nonContinuousSelection),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmessg.settings.NonContinuous')}</div>`,
        '</div><br/>',
        `<div id="WMESSG-div-sanityCheck" class="controls-container"><select id="WMESSG-sanityCheck" style="font-size:11px;height:22px;" title="${I18n.t('wmessg.settings.SanityCheckTitle')}">`,
        buildSelections(_settings.sanityCheck),
        `</select><div style="display:inline-block;font-size:11px;">${I18n.t('wmessg.settings.SanityCheck')}</div>`,
        '</div>'
    ].join(' '));
    new WazeWrap.Interface.Tab('SSG', $ssgTab.html(), registerEvents);
    _updateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
    _moveNode = require('Waze/Action/MoveNode');
    W.selectionManager.events.register('selectionchanged', null, insertSimplifyStreetGeometryButtons);
    $('#sidebar').on('click', '#WME-SSG', e => {
        e.preventDefault();
        doSimplifyStreetGeometry();
    });
    showScriptInfoAlert();
    log(`Fully initialized in ${Math.round(performance.now() - LOAD_BEGIN_TIME)} ms.`);
}

function bootstrap(tries) {
    if (W && W.map && W.model && $ && WazeWrap.Ready) {
        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);