WME BDP Check

Check for possible BDP routes between two selected segments.

当前为 2021-07-28 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/* eslint-disable no-nested-ternary */
// ==UserScript==
// @name        WME BDP Check
// @namespace   https://greasyfork.org/users/166843
// @version     2021.07.28.02
// @description Check for possible BDP routes between two selected segments.
// @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==

/* global document, localStorage, MutationObserver, window, $, performance, 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=294789',
    SCRIPT_GF_URL = 'https://greasyfork.org/en/scripts/393407-wme-bdp-check',
    SCRIPT_NAME = GM_info.script.name.replace('(beta)', 'β'),
    SCRIPT_VERSION = GM_info.script.version,
    SCRIPT_VERSION_CHANGES = ['<b>NEW:</b> Check detour selection for unroutable segment types.',
        '<b>CHANGE:</b> WME map object references.',
        '<b>CHANGE:</b> Routes must be selected by clicking first bracketing segment first and second bracketing segment last.',
        '<b>BUGFIX:</b> Zoom levels 1-3 do not contain LS or PS segments.',
        '<b>BUGFIX:</b> Better handling of multiple segments in detour route connected to same final node.'],
    SETTINGS_STORE_NAME = 'WMEBDPC',
    sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)),
    _timeouts = { bootstrap: undefined, saveSettingsToStorage: undefined },
    _editPanelObserver = new MutationObserver(mutations => {
        if ((W.selectionManager.getSegmentSelection().segments.length === 0) || ($('#WME-BDPC-BUTTONS-DIV').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 (W.selectionManager.getSegmentSelection().segments.length < 2)
                insertCheckBDPButton(true);
            else
                insertCheckBDPButton();
        }
    });
let _settings = {},
    _pathEndSegId,
    _restoreZoomLevel,
    _restoreMapCenter;

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

async function loadSettingsFromStorage() {
    const defaultSettings = {
            lastSaved: 0,
            lastVersion: undefined
        },
        loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME)),
        serverSettings = await WazeWrap.Remote.RetrieveSettings(SETTINGS_STORE_NAME);
    _settings = $.extend({}, defaultSettings, loadedSettings);
    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 showScriptInfoAlert() {
    if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
        let releaseNotes = '';
        releaseNotes += '<p>What\'s new:</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>Nothing major.</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 getMidpoint(startSeg, endSeg) {
    let startCenter = startSeg.getCenter(),
        endCenter = endSeg.getCenter();
    startCenter = WazeWrap.Geometry.ConvertTo4326(startCenter.x, startCenter.y);
    endCenter = WazeWrap.Geometry.ConvertTo4326(endCenter.x, endCenter.y);
    let lon1 = startCenter.lon,
        lat1 = startCenter.lat,
        lat2 = endCenter.lat;
    const piDiv = Math.PI / 180,
        divPi = 180 / Math.PI,
        lon2 = endCenter.lon,
        dLon = ((lon2 - lon1) * piDiv);
    lat1 *= piDiv;
    lat2 *= piDiv;
    lon1 *= piDiv;
    const bX = Math.cos(lat2) * Math.cos(dLon),
        bY = Math.cos(lat2) * Math.sin(dLon),
        lat3 = (Math.atan2(Math.sin(lat1) + Math.sin(lat2), Math.sqrt((Math.cos(lat1) + bX) * (Math.cos(lat1) + bX) + bY * bY))) * divPi,
        lon3 = (lon1 + Math.atan2(bY, Math.cos(lat1) + bX)) * divPi;
    return WazeWrap.Geometry.ConvertTo900913(lon3, lat3);
}

async function doZoom(restore = false, zoom = -1, coordObj = {}) {
    if ((zoom === -1) || (Object.entries(coordObj).length === 0))
        return Promise.resolve();
    W.map.setCenter(coordObj);
    if (W.map.getZoom() !== zoom)
        W.map.getOLMap().zoomTo(zoom);
    if (restore) {
        _restoreZoomLevel = undefined;
        _restoreMapCenter = undefined;
    }
    else {
        WazeWrap.Alerts.info(SCRIPT_NAME, 'Waiting for WME to populate after zoom level change.<br>Proceeding in 2 seconds...');
        await sleep(2000);
        $('#toast-container-wazedev > .toast-info').find('.toast-close-button').click();
    }
    return Promise.resolve();
}

function rtgContinuityCheck(segs = []) {
    if (segs.length < 2)
        return false;
    const rtg = { 7: 'mH', 6: 'MHFW', 3: 'MHFW' },
        seg1rtg = rtg[segs[0].attributes.roadType];
    segs.splice(0, 1);
    return segs.every(el => seg1rtg === rtg[el.attributes.roadType]);
}

function nameContinuityCheck(segs = []) {
    if (segs.length < 2)
        return false;
    const bs1StreetNames = [],
        bs2StreetNames = [],
        streetNames = [];
    let street;
    if (segs[0].attributes.primaryStreetID) {
        street = W.model.streets.getObjectById(segs[0].attributes.primaryStreetID);
        if (street && street.name && (street.name.length > 0)) {
            if (segs.length === 2)
                streetNames.push(street.name);
            else
                bs1StreetNames.push(street.name);
        }
    }
    if (segs[0].attributes.streetIDs.length > 0) {
        for (let i = 0; i < segs[0].attributes.streetIDs.length; i++) {
            street = W.model.streets.getObjectById(segs[0].attributes.streetIDs[i]);
            if (street && street.name && (street.name.length > 0)) {
                if (segs.length === 2)
                    streetNames.push(street.name);
                else
                    bs1StreetNames.push(street.name);
            }
        }
    }
    if (((segs.length === 2) && (streetNames.length === 0))
        || ((segs.length > 2) && (bs1StreetNames.length === 0)))
        return false;
    if (segs.length === 2) {
        if (segs[1].attributes.primaryStreetID) {
            street = W.model.streets.getObjectById(segs[1].attributes.primaryStreetID);
            if (street && street.name && streetNames.includes(street.name))
                return true;
        }
        if (segs[1].attributes.streetIDs.length > 0) {
            for (let i = 0; i < segs[1].attributes.streetIDs.length; i++) {
                street = W.model.streets.getObjectById(segs[1].attributes.streetIDs[i]);
                if (street && street.name && streetNames.includes(street.name))
                    return true;
            }
        }
    }
    else {
        segs.splice(0, 1);
        const lastIdx = segs.length - 1;
        if (segs[lastIdx].attributes.primaryStreetID) {
            street = W.model.streets.getObjectById(segs[lastIdx].attributes.primaryStreetID);
            if (street && street.name && (street.name.length > 0))
                bs2StreetNames.push(street.name);
        }
        if (segs[lastIdx].attributes.streetIDs.length > 0) {
            for (let i = 0; i < segs[lastIdx].attributes.streetIDs.length; i++) {
                street = W.model.streets.getObjectById(segs[lastIdx].attributes.streetIDs[i]);
                if (street && street.name && (street.name.length > 0))
                    bs2StreetNames.push(street.name);
            }
        }
        if (bs2StreetNames.length === 0)
            return false;
        segs.splice(-1, 1);
        return segs.every(el => {
            if (el.attributes.primaryStreetID) {
                street = W.model.streets.getObjectById(el.attributes.primaryStreetID);
                if (street && street.name && (bs1StreetNames.includes(street.name) || bs2StreetNames.includes(street.name)))
                    return true;
            }
            if (el.attributes.streetIDs.length > 0) {
                for (let i = 0; i < el.attributes.streetIDs.length; i++) {
                    street = W.model.streets.getObjectById(el.attributes.streetIDs[i]);
                    if (street && street.name && (bs1StreetNames.includes(street.name) || bs2StreetNames.includes(street.name)))
                        return true;
                }
            }
            return false;
        });
    }
    return false;
}

async function findLiveMapRoutes(startSeg, endSeg, maxLength) {
    const start900913center = startSeg.getCenter(),
        end900913center = endSeg.getCenter(),
        start4326Center = WazeWrap.Geometry.ConvertTo4326(start900913center.x, start900913center.y),
        end4326Center = WazeWrap.Geometry.ConvertTo4326(end900913center.x, end900913center.y),
        url = (W.model.countries.getObjectById(235) || W.model.countries.getObjectById(40) || W.model.countries.getObjectById(182))
            ? '/RoutingManager/routingRequest'
            : W.model.countries.getObjectById(106)
                ? '/il-RoutingManager/routingRequest'
                : '/row-RoutingManager/routingRequest',
        data = {
            from: `x:${start4326Center.lon} y:${start4326Center.lat}`,
            to: `x:${end4326Center.lon} y:${end4326Center.lat}`,
            returnJSON: true,
            returnGeometries: true,
            returnInstructions: false,
            timeout: 60000,
            type: 'HISTORIC_TIME',
            nPaths: 6,
            clientVersion: '4.0.0',
            vehType: 'PRIVATE',
            options: 'AVOID_TOLL_ROADS:f,AVOID_PRIMARIES:f,AVOID_DANGEROUS_TURNS:f,AVOID_FERRIES:f,ALLOW_UTURNS:t'
        },
        returnRoutes = [];
    let jsonData;
    try {
        jsonData = await $.ajax({
            dataType: 'JSON',
            cache: false,
            url,
            data,
            traditional: true,
            dataFilter: retData => retData.replace(/NaN/g, '0')
        }).fail((response, textStatus, errorThrown) => {
            logWarning(`Route request failed ${(textStatus !== null ? `with ${textStatus}` : '')}\r\n${errorThrown}!`);
        });
    }
    catch (error) {
        logWarning(JSON.stringify(error));
        jsonData = { error };
    }
    if (!jsonData) {
        logWarning('No data returned.');
    }
    else if (jsonData.error !== undefined) {
        logWarning(((typeof jsonData.error === 'object') ? $.parseJSON(jsonData.error) : jsonData.error.replace('|', '\r\n')));
    }
    else {
        let routes = (jsonData.coords !== undefined) ? [jsonData] : [];
        if (jsonData.alternatives !== undefined)
            routes = routes.concat(jsonData.alternatives);
        routes.forEach(route => {
            const fullRouteSegIds = route.response.results.map(result => result.path.segmentId),
                fullRouteSegs = W.model.segments.getByIds(fullRouteSegIds);
            if (nameContinuityCheck(fullRouteSegs) && rtgContinuityCheck(fullRouteSegs)) {
                const routeDistance = route.response.results.map(result => result.length).slice(1, -1).reduce((a, b) => a + b);
                if (routeDistance < maxLength)
                    returnRoutes.push(route.response.results.map(result => result.path.segmentId));
            }
        });
    }
    return new Promise(resolve => resolve(returnRoutes));
}

function findDirectRoute(obj = {}) {
    const {
            maxLength, startSeg, startNode, endSeg, endNodeIds
        } = obj,
        processedSegs = [],
        sOutIds = startNode.attributes.segIDs.filter(segId => segId !== startSeg.attributes.id),
        segIdsFilter = (nextSegIds, alreadyProcessed) => nextSegIds.filter(value => alreadyProcessed.indexOf(value) === -1),
        getNextSegs = (nextSegIds, curSeg, nextNode) => {
            const rObj = { addPossibleRouteSegments: [] };
            for (let i = 0; i < nextSegIds.length; i++) {
                const nextSeg = W.model.segments.getObjectById(nextSegIds[i]);
                if (curSeg.isTurnAllowed(nextSeg, nextNode)
                    && nameContinuityCheck([curSeg, nextSeg])
                    && (nameContinuityCheck([startSeg, nextSeg]) || nameContinuityCheck([endSeg, nextSeg]))
                ) {
                    if (!processedSegs.some(o => (o.fromSegId === curSeg.attributes.id) && (o.toSegId === nextSegIds[i]))) {
                        rObj.addPossibleRouteSegments.push({ nextSegStartNode: nextNode, nextSeg });
                        break;
                    }
                }
            }
            return rObj;
        },
        returnRoutes = [];
    for (let i = 0, len = sOutIds.length; i < len; i++) {
        const sOut = W.model.segments.getObjectById(sOutIds[i]);
        if (startSeg.isTurnAllowed(sOut, startNode) && nameContinuityCheck([startSeg, sOut])) {
            const possibleRouteSegments = [{
                curSeg: startSeg,
                nextSegStartNode: startNode,
                nextSeg: sOut
            }];
            let curLength = 0;
            while (possibleRouteSegments.length > 0) {
                const idx = possibleRouteSegments.length - 1,
                    curSeg = possibleRouteSegments[idx].nextSeg,
                    curSegStartNode = possibleRouteSegments[idx].nextSegStartNode,
                    curSegEndNode = curSeg.getOtherNode(curSegStartNode),
                    curSegEndNodeSOutIds = segIdsFilter(curSegEndNode.attributes.segIDs, possibleRouteSegments.map(routeSeg => routeSeg.nextSeg.attributes.id));
                if ((endNodeIds.indexOf(curSegEndNode.attributes.id) > -1) && curSeg.isTurnAllowed(endSeg, curSegEndNode)) {
                    returnRoutes.push([startSeg.attributes.id].concat(possibleRouteSegments.map(routeSeg => routeSeg.nextSeg.attributes.id), [endSeg.attributes.id]));
                    possibleRouteSegments.splice(idx, 1);
                }
                else if ((curLength + curSeg.attributes.length) > maxLength) {
                    possibleRouteSegments.splice(idx, 1);
                    curLength -= curSeg.attributes.length;
                }
                else {
                    const nextSegObj = getNextSegs(curSegEndNodeSOutIds, curSeg, curSegEndNode);
                    if (nextSegObj.addPossibleRouteSegments.length > 0) {
                        curLength += curSeg.attributes.length;
                        possibleRouteSegments.push(nextSegObj.addPossibleRouteSegments[0]);
                        processedSegs.push({ fromSegId: curSeg.attributes.id, toSegId: nextSegObj.addPossibleRouteSegments[0].nextSeg.attributes.id });
                    }
                    else {
                        curLength -= curSeg.attributes.length;
                        possibleRouteSegments.splice(idx, 1);
                    }
                }
            }
            if (returnRoutes.length > 0)
                break;
        }
        else {
            processedSegs.push({ fromSegId: startSeg.attributes.id, toSegId: sOut.attributes.id });
        }
    }
    return returnRoutes;
}

async function doCheckBDP(viaLM = false) {
    const selectedFeatures = W.selectionManager.getSelectedFeatures(),
        segmentSelection = W.selectionManager.getSegmentSelection(),
        numSelectedFeatureSegments = selectedFeatures.filter(feature => feature.model.type === 'segment').length;
    let startSeg,
        endSeg,
        directRoutes = [];
    if ((segmentSelection.segments.length < 2) || (numSelectedFeatureSegments < 2)) {
        WazeWrap.Alerts.error(SCRIPT_NAME, 'You must select either the two <i>bracketing segments</i> or an entire detour route with <i>bracketing segments</i>.');
        return;
    }
    if (segmentSelection.multipleConnectedComponents && ((segmentSelection.segments.length > 2) || (numSelectedFeatureSegments > 2))) {
        WazeWrap.Alerts.error(SCRIPT_NAME,
            'If you select more than 2 segments, the selection of segments must be continuous.<br><br>'
            + 'Either select just the two bracketing segments or an entire detour route with bracketing segments.');
        return;
    }
    if (!segmentSelection.multipleConnectedComponents && (segmentSelection.segments.length === 2)) {
        WazeWrap.Alerts.error(SCRIPT_NAME, 'You selected only two segments and they connect to each other. There are no alternate routes.');
        return;
    }
    if (segmentSelection.segments.length === 2) {
        [startSeg, endSeg] = segmentSelection.segments;
    }
    else if (_pathEndSegId !== undefined) {
        if (segmentSelection.segments[0].attributes.id === _pathEndSegId) {
            [endSeg] = segmentSelection.segments;
            startSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
        }
        else {
            [startSeg] = segmentSelection.segments;
            endSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
        }
        const routeNodeIds = segmentSelection.segments.slice(1, -1).flatMap(segment => [segment.attributes.toNodeID, segment.attributes.fromNodeID]);
        if (routeNodeIds.some(nodeId => endSeg.attributes.fromNodeID === nodeId))
            endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.toNodeID };
        else
            endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.fromNodeID };
    }
    else {
        [startSeg] = segmentSelection.segments;
        endSeg = segmentSelection.segments[segmentSelection.segments.length - 1];
        const routeNodeIds = segmentSelection.segments.slice(1, -1).flatMap(segment => [segment.attributes.toNodeID, segment.attributes.fromNodeID]);
        if (routeNodeIds.some(nodeId => endSeg.attributes.fromNodeID === nodeId))
            endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.toNodeID };
        else
            endSeg.attributes.bdpcheck = { routeFarEndNodeId: endSeg.attributes.fromNodeID };
    }
    if ((startSeg.attributes.roadType < 3) || (startSeg.attributes.roadType === 4) || (startSeg.attributes.roadType === 5) || (startSeg.attributes.roadType > 7)
        || (endSeg.attributes.roadType < 3) || (endSeg.attributes.roadType === 4) || (endSeg.attributes.roadType === 5) || (endSeg.attributes.roadType > 7)
    ) {
        WazeWrap.Alerts.info(SCRIPT_NAME, 'At least one of the bracketing selected segments is not in the correct road type group for BDP.');
        return;
    }
    if (!rtgContinuityCheck([startSeg, endSeg])) {
        WazeWrap.Alerts.info(SCRIPT_NAME, 'One bracketing segment is a minor highway while the other is not. BDP only applies when bracketing segments are in the same road type group.');
        return;
    }
    if (!nameContinuityCheck([startSeg, endSeg])) {
        WazeWrap.Alerts.info(SCRIPT_NAME, 'The bracketing segments do not share a street name. BDP will not be applied to any route.');
        return;
    }
    const maxLength = (startSeg.attributes.roadType === 7) ? 5000 : 50000;
    if (segmentSelection.segments.length === 2) {
        if (((startSeg.attributes.roadType === 7) && (W.map.getZoom() > 4))
            || ((startSeg.attributes.roadType !== 7) && (W.map.getZoom() > 3))) {
            _restoreZoomLevel = W.map.getZoom();
            _restoreMapCenter = W.map.getCenter();
            await doZoom(false, (startSeg.attributes.roadType === 7) ? 4 : 3, getMidpoint(startSeg, endSeg));
        }
        if (viaLM) {
            directRoutes = directRoutes.concat(await findLiveMapRoutes(startSeg, endSeg, maxLength));
        }
        else {
            const startSegDirection = startSeg.getDirection(),
                endSegDirection = endSeg.getDirection();
            const startNodeObjs = [],
                endNodeObjs = [];
            if ((startSegDirection !== 2) && startSeg.getToNode())
                startNodeObjs.push(startSeg.getToNode());
            if ((startSegDirection !== 1) && startSeg.getFromNode())
                startNodeObjs.push(startSeg.getFromNode());
            if ((endSegDirection !== 2) && endSeg.getFromNode())
                endNodeObjs.push(endSeg.getFromNode());
            if ((endSegDirection !== 1) && endSeg.getToNode())
                endNodeObjs.push(endSeg.getToNode());
            for (let i = 0; i < startNodeObjs.length; i++) {
                const startNode = startNodeObjs[i];
                directRoutes = findDirectRoute({
                    maxLength, startSeg, startNode, endSeg, endNodeIds: endNodeObjs.map(nodeObj => nodeObj && nodeObj.attributes.id)
                });
                if (directRoutes.length > 0)
                    break;
            }
        }
    }
    else {
        const routeSegIds = W.selectionManager.getSegmentSelection().getSelectedSegments()
                .map(segment => segment.attributes.id)
                .filter(segId => (segId !== endSeg.attributes.id) && (segId !== startSeg.attributes.id)),
            endNodeObj = endSeg.getOtherNode(W.model.nodes.getObjectById(endSeg.attributes.bdpcheck.routeFarEndNodeId)),
            startSegDirection = startSeg.getDirection(),
            startNodeObjs = [],
            lastDetourSegId = routeSegIds.filter(el => endNodeObj.attributes.segIDs.includes(el));
        let lastDetourSeg;
        if (lastDetourSegId.length === 1) {
            lastDetourSeg = W.model.segments.getObjectById(lastDetourSegId);
        }
        else {
            const oneWayTest = W.model.segments.getByIds(lastDetourSegId).filter(seg => seg.isOneWay() && seg.isTurnAllowed(endSeg, endNodeObj));
            if (oneWayTest.length === 1) {
                [lastDetourSeg] = oneWayTest;
            }
            else {
                WazeWrap.Alerts.info(SCRIPT_NAME, `Could not determine the last detour segment. Please send ${SCRIPT_AUTHOR} a message with a PL describing this issue. Thank you!`);
                return;
            }
        }
        const detourSegs = segmentSelection.segments.slice(1, -1),
            detourSegTypes = [...new Set(detourSegs.map(segment => segment.attributes.roadType))];
        if ([9, 10, 16, 18, 19, 22].some(type => detourSegTypes.indexOf(type) > -1)) {
            WazeWrap.Alerts.info(SCRIPT_NAME, 'Your selection contains one more more segments with an unrouteable road type. The selected route is not a valid route.');
            return;
        }
        if (![1].some(type => detourSegTypes.indexOf(type) > -1)) {
            if (((startSeg.attributes.roadType === 7) && (W.map.getZoom() > 4))
                || ((startSeg.attributes.roadType !== 7) && (W.map.getZoom() > 3))) {
                _restoreZoomLevel = W.map.getZoom();
                _restoreMapCenter = W.map.getCenter();
                await doZoom(false, (startSeg.attributes.roadType === 7) ? 4 : 3, getMidpoint(startSeg, endSeg));
            }
        }
        if ((startSegDirection !== 2) && startSeg.getToNode())
            startNodeObjs.push(startSeg.getToNode());
        if ((startSegDirection !== 1) && startSeg.getFromNode())
            startNodeObjs.push(startSeg.getFromNode());
        if (nameContinuityCheck([lastDetourSeg, endSeg])) {
            WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because the last detour segment and the second bracketing segment share a common street name.');
            doZoom(true, _restoreZoomLevel, _restoreMapCenter);
            return;
        }
        if (rtgContinuityCheck([lastDetourSeg, endSeg])) {
            WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because the last detour segment and the second bracketing segment are in the same road type group.');
            doZoom(true, _restoreZoomLevel, _restoreMapCenter);
            return;
        }
        if (detourSegs.length < 2) {
            WazeWrap.Alerts.info(SCRIPT_NAME, 'BDP will not be applied to this detour route because it is less than 2 segments long.');
            doZoom(true, _restoreZoomLevel, _restoreMapCenter);
            return;
        }
        if (detourSegs.map(seg => seg.attributes.length).reduce((a, b) => a + b) > ((startSeg.attributes.roadType === 7) ? 500 : 5000)) {
            WazeWrap.Alerts.info(SCRIPT_NAME, `BDP will not be applied to this detour route because it is longer than ${((startSeg.attributes.roadType === 7) ? '500m' : '5km')}.`);
            doZoom(true, _restoreZoomLevel, _restoreMapCenter);
            return;
        }
        if (viaLM) {
            directRoutes = directRoutes.concat(await findLiveMapRoutes(startSeg, endSeg, maxLength));
        }
        else {
            for (let i = 0; i < startNodeObjs.length; i++) {
                const startNode = startNodeObjs[i];
                directRoutes = findDirectRoute({
                    maxLength, startSeg, startNode, endSeg, endNodeIds: [endNodeObj.attributes.id]
                });
                if (directRoutes.length > 0)
                    break;
            }
        }
    }
    if (directRoutes.length > 0) {
        WazeWrap.Alerts.confirm(SCRIPT_NAME,
            'A <b>direct route</b> was found! Would you like to select the direct route?',
            () => {
                const segments = [];
                for (let i = 0; i < directRoutes[0].length; i++) {
                    const seg = W.model.segments.getObjectById(directRoutes[0][i]);
                    if (seg !== 'undefined')
                        segments.push(seg);
                }
                W.selectionManager.setSelectedModels(segments);
                doZoom(true, _restoreZoomLevel, _restoreMapCenter);
            },
            () => { doZoom(true, _restoreZoomLevel, _restoreMapCenter); }, 'Yes', 'No');
    }
    else if (segmentSelection.segments.length === 2) {
        WazeWrap.Alerts.info(SCRIPT_NAME,
            'No direct routes found between the two selected segments. A BDP penalty <b>will not</b> be applied to any routes.'
                + '<br><b>Note:</b> This could also be caused by the distance between the two selected segments is longer than than the allowed distance for detours.');
        doZoom(true, _restoreZoomLevel, _restoreMapCenter);
    }
    else {
        WazeWrap.Alerts.info(SCRIPT_NAME,
            'No direct routes found between the possible detour bracketing segments. A BDP penalty <b>will not</b> be applied to the selected route.'
                + '<br><b>Note:</b> This could also be because any possible direct routes are very long, which would take longer to travel than taking the selected route (even with penalty).');
        doZoom(true, _restoreZoomLevel, _restoreMapCenter);
    }
}

function insertCheckBDPButton(remove = false) {
    const $wmeButton = $('#WME-BDPC-WME'),
        $lmButton = $('#WME-BDPC-LM'),
        $buttonsDiv = $('#WME-BDPC-BUTTONS-DIV');
    if (remove) {
        if ($buttonsDiv.length > 0)
            $buttonsDiv.remove();
        return;
    }
    let htmlOut = '';
    if ($buttonsDiv.length === 0)
        htmlOut += '<div id="WME-BDPC-BUTTONS-DIV" style="margin:0 0 10px 10px;">';
    if ($wmeButton.length === 0)
        htmlOut += '<button id="WME-BDPC-WME" class="waze-btn waze-btn-small waze-btn-white" title="Check BDP of selected segments, via WME.">BDP Check (WME)</button>';
    if ($lmButton.length === 0)
        htmlOut += '<button id="WME-BDPC-LM" class="waze-btn waze-btn-small waze-btn-white" title="Check BDP of selected segments, via LM.">BDP Check (LM)</button>';
    if ($buttonsDiv.length === 0)
        htmlOut += '</div>';
    if (htmlOut !== '')
        $(htmlOut).insertAfter($('#edit-panel .segment .selection'));
}

function pathSelected(evt) {
    if (evt && evt.feature && evt.feature.model && (evt.feature.model.type === 'segment'))
        _pathEndSegId = evt.feature.model.attributes.id;
}

async function init() {
    log('Initializing.');
    await loadSettingsFromStorage();
    _editPanelObserver.observe(document.querySelector('#edit-panel > div'), {
        childList: true, attributes: false, attributeOldValue: false, characterData: false, characterDataOldValue: false, subtree: true
    });
    W.selectionManager.selectionMediator.on('map:selection:pathSelect', pathSelected);
    W.selectionManager.selectionMediator.on('map:selection:featureClick', () => { _pathEndSegId = undefined; });
    W.selectionManager.selectionMediator.on('map:selection:clickOut', () => { _pathEndSegId = undefined; });
    W.selectionManager.selectionMediator.on('map:selection:deselectKey', () => { _pathEndSegId = undefined; });
    W.selectionManager.selectionMediator.on('map:selection:featureBoxSelection', () => { _pathEndSegId = undefined; });
    if (W.selectionManager.getSegmentSelection().segments.length > 1) {
        $('.tabs-container').before(
            '   <div id="WME-BDPC-BUTTONS-DIV" style="margin:0 0 10px 10px;">'
            + '     <button id="WME-BDPC-WME" class="waze-btn waze-btn-small waze-btn-white" title="Check BDP of selected segments, via WME.">BDP Check (WME)</button>'
            + '     <button id="WME-BDPC-LM" class="waze-btn waze-btn-small waze-btn-white" title="Check BDP of selected segments, via LM." >BDP Check (LM)</button>'
            + ' </div>'
        );
    }
    $('#sidebar').on('click', '#WME-BDPC-WME', e => {
        e.preventDefault();
        doCheckBDP(false);
    });
    $('#sidebar').on('click', '#WME-BDPC-LM', e => {
        e.preventDefault();
        doCheckBDP(true);
    });
    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);