WME BDP Check

Check for possible BDP routes between two selected segments.

目前為 2021-07-28 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);