WME HN NavPoints

Shows navigation points of all house numbers in WME

目前为 2021-08-30 提交的版本。查看 最新版本

/* eslint-disable no-template-curly-in-string */
// ==UserScript==
// @name            WME HN NavPoints
// @namespace       https://greasyfork.org/users/166843
// @description     Shows navigation points of all house numbers in WME
// @version         2021.08.30.01
// @author          dBsooner
// @grant           none
// @require         https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @license         GPLv3
// @include         /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @contributionURL https://github.com/WazeDev/Thank-The-Authors
// ==/UserScript==

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

/*
 * Original concept and code for WME HN NavPoints was written by MajkiiTelini. After version 0.6.6, this
 * script is maintained by the WazeDev team. Special thanks is definitely given to MajkiiTelini for his
 * hard work and dedication to the original script.
 *
 */

const ALERT_UPDATE = true,
    DEBUG = false,
    LOAD_BEGIN_TIME = performance.now(),
    SCRIPT_FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=269397',
    SCRIPT_GF_URL = 'https://greasyfork.org/en/scripts/390565-wme-hn-navpoints',
    SCRIPT_NAME = GM_info.script.name.replace('(beta)', 'β'),
    SCRIPT_VERSION = GM_info.script.version,
    SCRIPT_VERSION_CHANGES = [
        '<b>CHANGE:</b> Update zoom levels to new WME numbers.',
        '<b>BUGFIX:</b> Changed forum post URL.'
    ],
    SETTINGS_STORE_NAME = 'WMEHNNavPoints',
    _spinners = {
        destroyAllHNs: false,
        drawHNs: false,
        processSegs: false
    },
    _timeouts = {
        bootstrap: undefined,
        hideTooltip: undefined,
        saveSettingsToStorage: undefined,
        setMarkersEvents: undefined
    };

let _settings = {},
    _scriptActive = false,
    _HNLayerObserver,
    _saveButtonObserver,
    _HNNavPointsLayer,
    _HNNavPointsNumbersLayer,
    _wmeHnLayer,
    _processedSegments = [],
    _segmentsToProcess = [],
    _segmentsToRemove = [],
    _$hnNavPointsTooltipDiv,
    /* 2020.07.16.01 - Removed in favor of dual layer types: one for Vector (no tooltip popup) and one for marker (tooltip popup).
       Prior to this it was an attempt to work with several OL SelectFeatures controllers. However, it doesn't seem possible with OL 2.
     _hnMouseoverCtrl,
    */
    _popup = {
        inUse: false,
        hnNumber: -1,
        segmentId: -1
    };

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

async function loadSettingsFromStorage() {
    const defaultSettings = {
            disableBelowZoom: 17,
            enableTooltip: true,
            hnLines: true,
            hnNumbers: true,
            keepHNLayerOnTop: true,
            toggleHNNavPointsShortcut: '',
            toggleHNNavPointsNumbersShortcut: '',
            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);
    if (_settings.disableBelowZoom < 11)
        _settings.disableBelowZoom += 12;
    _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 checkShortcutsChanged() {
    let triggerSave = false;
    ['toggleHNNavPointsShortcut', 'toggleHNNavPointsNumbersShortcut'].forEach(k => {
        let keys = '';
        const { shortcut } = W.accelerators.Actions[k];
        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[k] !== keys) {
            _settings[k] = keys;
            triggerSave = true;
        }
    });
    if (triggerSave)
        saveSettingsToStorage();
}

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 doSpinner(spinnerName = '', spin = true) {
    const $btn = $('#hnNPSpinner');
    if (!spin) {
        _spinners[spinnerName] = false;
        if (!Object.values(_spinners).some(a => a === true)) {
            if ($btn.length > 0) {
                $btn.removeClass('fa-spin');
                $('#divHnNPSpinner').hide();
            }
            else {
                $('#topbar-container .topbar').prepend(
                    '<div id="divHnNPSpinner" title="WME HN NavPoints is currently processing house numbers." style="font-size:20px;background:white;float:left;margin-left:-20px;display:none;">'
                    + '<i id="hnNPSpinner" class="fa fa-spinner"></i></div>'
                );
            }
        }
    }
    else {
        _spinners[spinnerName] = true;
        if ($btn.length === 0) {
            _spinners[spinnerName] = true;
            $('#topbar-container .topbar').prepend(
                '<div id="divHnNPSpinner" title="WME HN NavPoints is currently processing house numbers." style="font-size:20px;background:white;float:left;margin-left:-20px;">'
                + '<i id="hnNPSpinner" class="fa fa-spinner fa-spin"></i></div>'
            );
        }
        else if (!$btn.hasClass('fa-spin')) {
            $btn.addClass('fa-spin');
            $('#divHnNPSpinner').show();
        }
    }
}

function processSegmentsToRemove() {
    if (_segmentsToRemove.length > 0) {
        const removeMarker = marker => { _HNNavPointsNumbersLayer.removeMarker(marker); };
        let linesToRemove = [],
            hnsToRemove = [];
        for (let i = _segmentsToRemove.length - 1; i > -1; i--) {
            const segId = _segmentsToRemove[i];
            if (!W.model.segments.objects[segId]) {
                _segmentsToRemove.splice(i, 1);
                linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('segmentId', segId));
                if (!_settings.enableTooltip)
                    hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', segId));
                else
                    _HNNavPointsNumbersLayer.markers.filter(marker => marker.segmentId === segId).forEach(marker => removeMarker(marker));
            }
        }
        if (linesToRemove.length > 0)
            _HNNavPointsLayer.removeFeatures(linesToRemove);
        if (hnsToRemove.length > 0)
            _HNNavPointsNumbersLayer.removeFeatures(hnsToRemove);
    }
}

async function hnLayerToggled(checked) {
    _HNNavPointsLayer.setVisibility(checked);
    _settings.hnLines = checked;
    saveSettingsToStorage();
    if (checked) {
        if (!_scriptActive)
            await initBackgroundTasks('enable');
        processSegs('hnLayerToggled', W.model.segments.getByAttributes({ hasHNs: true }));
    }
    else if (!_settings.hnNumbers && _scriptActive) {
        initBackgroundTasks('disable');
    }
}

async function hnNumbersLayerToggled(checked) {
    _HNNavPointsNumbersLayer.setVisibility(checked);
    _settings.hnNumbers = checked;
    saveSettingsToStorage();
    if (checked) {
        if (!_scriptActive)
            await initBackgroundTasks('enable');
        processSegs('hnNumbersLayerToggled', W.model.segments.getByAttributes({ hasHNs: true }));
    }
    else if (!_settings.hnLines && _scriptActive) {
        initBackgroundTasks('disable');
    }
}

function observeHNLayer() {
    if (W.editingMediator.attributes.editingHouseNumbers && !_HNLayerObserver.observing) {
        [_wmeHnLayer] = W.map.getLayersByName('houseNumberMarkers');
        _HNLayerObserver.observe(_wmeHnLayer.div, { childList: false, subtree: true, attributes: true });
        _HNLayerObserver.observing = true;
    }
    else if (_HNLayerObserver.observing) {
        _HNLayerObserver.disconnect();
        _HNLayerObserver.observing = false;
    }
    if (!_HNLayerObserver.observing) {
        W.model.segmentHouseNumbers.clear();
        processSegs('exithousenumbers', W.model.segments.getByIds(_segmentsToProcess), true);
        processSegmentsToRemove();
        _wmeHnLayer = undefined;
    }
    else {
        _segmentsToProcess = W.selectionManager.getSegmentSelection().segments.map(segment => segment.attributes.id);
        _segmentsToRemove = [];
    }
    _saveButtonObserver.disconnect();
    _saveButtonObserver.observe($('#edit-buttons .waze-icon-save')[0], {
        childList: false, attributes: true, attributeOldValue: true, characterData: false, characterDataOldValue: false, subtree: false
    });
}

function removeHNs(objArr) {
    let linesToRemove = [],
        hnsToRemove = [];
    objArr.forEach(hnObj => {
        linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id));
        if (!_settings.enableTooltip)
            hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id));
        else
            _HNNavPointsNumbersLayer.markers.filter(a => a.featureId === hnObj.attributes.id).forEach(marker => { _HNNavPointsNumbersLayer.removeMarker(marker); });
    });
    if (linesToRemove.length > 0)
        _HNNavPointsLayer.removeFeatures(linesToRemove);
    if (hnsToRemove.length > 0)
        _HNNavPointsNumbersLayer.removeFeatures(hnsToRemove);
}

function drawHNs(houseNumberArr) {
    if (houseNumberArr.length === 0)
        return;
    doSpinner('drawHNs', true);
    const lineFeatures = [],
        numberFeatures = !_settings.enableTooltip ? [] : undefined,
        svg = _settings.enableTooltip ? document.createElementNS('http://www.w3.org/2000/svg', 'svg') : undefined,
        svgText = _settings.enableTooltip ? document.createElementNS('http://www.w3.org/2000/svg', 'text') : undefined,
        invokeTooltip = _settings.enableTooltip ? evt => { showTooltip(evt); } : undefined;
    if (_settings.enableTooltip) {
        svg.setAttribute('xlink', 'http://www.w3.org/1999/xlink');
        svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        svg.setAttribute('viewBox', '0 0 40 14');
        svgText.setAttribute('text-anchor', 'middle');
        svgText.setAttribute('x', '20');
        svgText.setAttribute('y', '10');
    }
    for (let i = 0; i < houseNumberArr.length; i++) {
        const hnObj = houseNumberArr[i],
            segmentId = hnObj.getSegmentId(),
            seg = W.model.segments.objects[segmentId];
        if (seg) {
            const featureId = hnObj.getID(),
                markerIdx = _settings.enableTooltip ? _HNNavPointsNumbersLayer.markers.map(marker => marker.featureId).indexOf(featureId) : undefined,
                // eslint-disable-next-line no-nested-ternary
                hnToRemove = _settings.enableTooltip ? (markerIdx > -1) ? _HNNavPointsNumbersLayer.markers[markerIdx] : [] : _HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', featureId),
                rtlChar = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg,
                textDir = (hnObj.getNumber().match(rtlChar) !== null) ? 'rtl' : 'ltr';
            _HNNavPointsLayer.removeFeatures(_HNNavPointsLayer.getFeaturesByAttribute('featureId', featureId));
            if (hnToRemove.length > 0) {
                if (_settings.enableTooltip)
                    _HNNavPointsNumbersLayer.removeMarker(hnToRemove);
                else
                    _HNNavPointsNumbersLayer.removeFeatures(_HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', featureId));
            }
            const p1 = new OpenLayers.Geometry.Point(hnObj.getFractionPoint().x, hnObj.getFractionPoint().y),
                p2 = new OpenLayers.Geometry.Point(hnObj.getGeometry().x, hnObj.getGeometry().y),
                // eslint-disable-next-line no-nested-ternary
                strokeColor = (hnObj.isForced()
                    ? (!hnObj.getUpdatedBy()) ? 'red' : 'orange'
                    : (!hnObj.getUpdatedBy()) ? 'yellow' : 'white'
                );
            let lineString = new OpenLayers.Geometry.LineString([p1, p2]),
                lineFeature = new OpenLayers.Feature.Vector(
                    lineString,
                    { segmentId, featureId },
                    {
                        strokeWidth: 4, strokeColor: 'black', strokeOpacity: 0.5, strokeDashstyle: 'dash', strokeDashArray: '8, 8'
                    }
                );
            lineFeatures.push(lineFeature);
            lineString = new OpenLayers.Geometry.LineString([p1, p2]);
            lineFeature = new OpenLayers.Feature.Vector(
                lineString,
                { segmentId, featureId },
                {
                    strokeWidth: 2, strokeColor, strokeOpacity: 1, strokeDashstyle: 'dash', strokeDashArray: '8, 8'
                }
            );
            lineFeatures.push(lineFeature);
            if (_settings.enableTooltip) {
                svg.setAttribute('style', `text-shadow:0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor},0 0 3px ${strokeColor};font-size:14px;font-weight:bold;font-family:"Open Sans", "Arial Unicode MS", "sans-serif";direction:${textDir}`);
                svgText.textContent = hnObj.getNumber();
                svg.innerHTML = svgText.outerHTML;
                const svgIcon = new WazeWrap.Require.Icon(`data:image/svg+xml,${svg.outerHTML}`, { w: 40, h: 18 }),
                    markerFeature = new OpenLayers.Marker(new OpenLayers.LonLat(p2.x, p2.y), svgIcon);
                markerFeature.events.register('mouseover', null, invokeTooltip);
                markerFeature.events.register('mouseout', null, hideTooltipDelay);
                markerFeature.featureId = featureId;
                markerFeature.segmentId = segmentId;
                markerFeature.hnNumber = hnObj.getNumber() || '';
                _HNNavPointsNumbersLayer.addMarker(markerFeature);
            }
            else {
                // eslint-disable-next-line new-cap
                numberFeatures.push(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Polygon.createRegularPolygon(p2, 1, 20), {
                    segmentId, featureId, hn_number: hnObj.getNumber(), strokeWidth: 3, Color: strokeColor, textDir
                }));
            }
        }
    }
    if (lineFeatures.length > 0)
        _HNNavPointsLayer.addFeatures(lineFeatures);
    if (!_settings.enableTooltip && (numberFeatures.length > 0))
        _HNNavPointsNumbersLayer.addFeatures(numberFeatures);
    doSpinner('drawHNs', false);
}

function destroyAllHNs() {
    doSpinner('destroyAllHNs', true);
    _HNNavPointsLayer.destroyFeatures();
    if (_settings.enableTooltip)
        _HNNavPointsNumbersLayer.clearMarkers();
    else
        _HNNavPointsNumbersLayer.destroyFeatures();
    _processedSegments = [];
    doSpinner('destroyAllHNs', false);
    Promise.resolve();
}

function processSegs(action, arrSegObjs, processAll = false, retry = 0) {
    /* As of 2020.06.08 (sometime before this date) updatedOn does not get updated when updating house numbers. Looking for a new
     * way to track which segments have been updated most recently to prevent a total refresh of HNs after an event.
     * Changed to using a global to keep track of segmentIds touched during HN edit mode.
     */
    if ((action === 'settingChanged') && (W.map.getZoom() < _settings.disableBelowZoom)) {
        destroyAllHNs();
        return;
    }
    if (!arrSegObjs || (arrSegObjs.length === 0) || (W.map.getZoom() < _settings.disableBelowZoom) || preventProcess())
        return;
    doSpinner('processSegs', true);
    const eg = W.map.getExtent().toGeometry(),
        findObjIndex = (array, fldName, value) => array.map(a => a[fldName]).indexOf(value),
        processError = (err, chunk) => {
            logDebug(`Retry: ${retry}`);
            if (retry < 5)
                processSegs(action, chunk, true, ++retry);
            else
                logError(`Get HNs for ${chunk.length} segments failed. Code: ${err.status} - Text: ${err.responseText}`);
        },
        processJSON = jsonData => {
            if (jsonData && (jsonData.error === undefined) && (typeof jsonData.segmentHouseNumbers.objects !== 'undefined'))
                drawHNs(jsonData.segmentHouseNumbers.objects);
        };
    if ((action === 'objectsremoved')) {
        if (arrSegObjs && (arrSegObjs.length > 0)) {
            const removedSegIds = [];
            let hnNavPointsToRemove = [],
                hnNavPointsNumbersToRemove = [];
            arrSegObjs.forEach(segObj => {
                const segmentId = segObj.getID();
                if (!eg.intersects(segObj.geometry) && (segmentId > 0)) {
                    hnNavPointsToRemove = hnNavPointsToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('segmentId', segmentId));
                    if (!_settings.enableTooltip)
                        hnNavPointsNumbersToRemove = hnNavPointsNumbersToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', segmentId));
                    else
                        removedSegIds.push(segmentId);
                    const segIdx = findObjIndex(_processedSegments, 'segId', segmentId);
                    if (segIdx > -1)
                        _processedSegments.splice(segIdx, 1);
                }
            });
            if (hnNavPointsToRemove.length > 0)
                _HNNavPointsLayer.removeFeatures(hnNavPointsToRemove);
            if (hnNavPointsNumbersToRemove.length > 0)
                _HNNavPointsNumbersLayer.removeFeatures(hnNavPointsNumbersToRemove);
            if (removedSegIds.length > 0) {
                _HNNavPointsNumbersLayer.markers.filter(marker => removedSegIds.includes(marker.segmentId)).forEach(marker => {
                    _HNNavPointsNumbersLayer.removeMarker(marker);
                });
            }
        }
    }
    else { // action = 'objectsadded', 'zoomend', 'init', 'exithousenumbers', 'hnLayerToggled', 'hnNumbersLayerToggled', 'settingChanged', 'afterSave'
        let i = arrSegObjs.length;
        while (i--) {
            if (arrSegObjs[i].getID() < 0) {
                arrSegObjs.splice(i, 1);
            }
            else {
                const segIdx = findObjIndex(_processedSegments, 'segId', arrSegObjs[i].getID());
                if (segIdx > -1) {
                    if (arrSegObjs[i].getUpdatedOn() > _processedSegments[segIdx].updatedOn)
                        _processedSegments[segIdx].updatedOn = arrSegObjs[i].getUpdatedOn();
                    else if (!processAll)
                        arrSegObjs.splice(i, 1);
                }
                else {
                    _processedSegments.push({ segId: arrSegObjs[i].getID(), updatedOn: arrSegObjs[i].getUpdatedOn() });
                }
            }
        }
        while (arrSegObjs.length > 0) {
            let chunk;
            if (retry === 1)
                chunk = arrSegObjs.splice(0, 250);
            else if (retry === 2)
                chunk = arrSegObjs.splice(0, 125);
            else if (retry === 3)
                chunk = arrSegObjs.splice(0, 100);
            else if (retry === 4)
                chunk = arrSegObjs.splice(0, 50);
            else
                chunk = arrSegObjs.splice(0, 500);
            try {
                W.controller.descartesClient.getHouseNumbers(chunk.map(segObj => segObj.getID())).then(processJSON).catch(error => processError(error, [...chunk]));
            }
            catch (error) {
                processError(error, [...chunk]);
            }
        }
    }
    doSpinner('processSegs', false);
}

function preventProcess() {
    if (!_settings.hnLines && !_settings.hnNumbers) {
        if (_scriptActive)
            initBackgroundTasks('disable');
        destroyAllHNs();
        return true;
    }
    if (W.map.getZoom() < _settings.disableBelowZoom) {
        destroyAllHNs();
        return true;
    }
    return false;
}

function markerEvent(evt) {
    if (!evt || preventProcess())
        return;
    if (evt.type === 'click:input') {
        if (evt.object && evt.object.dragging && !evt.object.dragging.last)
            removeHNs([evt.object.model]);
    }
    else if (evt.type === 'delete') {
        removeHNs([evt.object.model]);
    }
}

function setMarkersEvents() {
    if (W.editingMediator.attributes.editingHouseNumbers) {
        checkTimeout({ timeout: 'setMarkersEvents' });
        hideTooltip();
        if (!_wmeHnLayer || (_wmeHnLayer && (_wmeHnLayer.markers.length === 0))) {
            _timeouts.setMarkersEvents = window.setTimeout(setMarkersEvents, 50);
            return;
        }
        _wmeHnLayer.markers.forEach(marker => {
            marker.events.unregister('click:input', null, markerEvent);
            marker.events.unregister('delete', null, markerEvent);
            marker.events.on({ 'click:input': markerEvent, delete: markerEvent });
        });
    }
    else if (_wmeHnLayer) {
        _wmeHnLayer.markers.forEach(marker => {
            marker.events.unregister('click:input', null, markerEvent);
            marker.events.unregister('delete', null, markerEvent);
        });
    }
}

function checkMarkersEvents() {
    if (_wmeHnLayer && (_wmeHnLayer.markers.length > 0) && !_wmeHnLayer.markers[0].events.listeners['click:input'].some(callbackFn => callbackFn.func === markerEvent))
        setMarkersEvents();
}

function segmentsEvent(evt) {
    if (!evt || preventProcess())
        return;
    if ((this.action === 'objectssynced') || (this.action === 'objectsremoved'))
        processSegmentsToRemove();
    if (this.action === 'objectschanged-id') {
        const oldSegmentId = evt.oldID,
            newSegmentID = evt.newID;
        _HNNavPointsLayer.getFeaturesByAttribute('segmentId', oldSegmentId).forEach(feature => { feature.attributes.segmentId = newSegmentID; });
        if (_settings.enableTooltip)
            _HNNavPointsNumbersLayer.markers.filter(marker => marker.segmentId === oldSegmentId).forEach(marker => { marker.segmentId = newSegmentID; });
        else
            _HNNavPointsNumbersLayer.getFeaturesByAttribute('segmentId', oldSegmentId).forEach(feature => { feature.attributes.segmentId = newSegmentID; });
    }
    else if (this.action === 'objects-state-deleted') {
        evt.forEach(obj => {
            if (_segmentsToRemove.indexOf(obj.getID()) === -1)
                _segmentsToRemove.push(obj.getID());
        });
    }
    else {
        processSegs(this.action, evt.filter(seg => seg.attributes.hasHNs));
    }
}

function objectsChangedIdHNs(evt) {
    if (!evt || preventProcess())
        return;
    const oldFeatureId = evt.oldID,
        newFeatureId = evt.newID;
    _HNNavPointsLayer.getFeaturesByAttribute('featureId', oldFeatureId).forEach(feature => { feature.attributes.featureId = newFeatureId; });
    if (_settings.enableTooltip)
        _HNNavPointsNumbersLayer.markers.filter(marker => marker.featureId === oldFeatureId).forEach(marker => { marker.featureId = newFeatureId; });
    else
        _HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', oldFeatureId).forEach(feature => { feature.attributes.featureId = newFeatureId; });
}

function objectsChangedHNs(evt) {
    if (!evt || preventProcess())
        return;
    if ((evt.length === 1) && evt[0].getSegmentId() && (_segmentsToProcess.indexOf(evt[0].getSegmentId()) === -1))
        _segmentsToProcess.push(evt[0].getSegmentId());
    checkMarkersEvents();
}

function objectsStateDeletedHNs(evt) {
    if (!evt || preventProcess())
        return;
    if ((evt.length === 1) && evt[0].getSegmentId() && (_segmentsToProcess.indexOf(evt[0].getSegmentId()) === -1))
        _segmentsToProcess.push(evt[0].getSegmentId());
    removeHNs(evt);
    checkMarkersEvents();
}

function objectsAddedHNs(evt) {
    if (!evt || preventProcess())
        return;
    if ((evt.length === 1) && evt[0].getSegmentId() && (_segmentsToProcess.indexOf(evt[0].getSegmentId()) === -1))
        _segmentsToProcess.push(evt[0].getSegmentId());
    checkMarkersEvents();
}

function zoomEndEvent() {
    if (preventProcess())
        return;
    if ((W.map.getZoom() < _settings.disableBelowZoom))
        destroyAllHNs();
    if ((W.map.getZoom() > (_settings.disableBelowZoom - 1)) && (_processedSegments.length === 0))
        processSegs('zoomend', W.model.segments.getByAttributes({ hasHNs: true }), true);
}

function afterActionsEvent(evt) {
    if (!evt || preventProcess())
        return;
    if ((evt.type === 'afterclearactions') || (evt.type === 'noActions')) {
        processSegmentsToRemove();
    }
    else if (evt.action._description && (evt.action._description.indexOf('Deleted house number') > -1)) {
        if (evt.type === 'afterundoaction')
            drawHNs([evt.action.object]);
        else
            removeHNs(evt.action.object);
        setMarkersEvents();
    }
    else if (evt.action._description && (evt.action._description.indexOf('Updated house number') > -1)) {
        const tempEvt = _.cloneDeep(evt);
        if (evt.type === 'afterundoaction') {
            if (tempEvt.action.newAttributes && tempEvt.action.newAttributes.number)
                tempEvt.action.attributes.number = tempEvt.action.newAttributes.number;
        }
        else if (evt.type === 'afteraction') {
            if (tempEvt.action.oldAttributes && tempEvt.action.oldAttributes.number)
                tempEvt.action.attributes.number = tempEvt.action.oldAttributes.number;
        }
        removeHNs(tempEvt.action.object);
        drawHNs([evt.action.object]);
        setMarkersEvents();
    }
    else if (evt.action._description && (evt.action._description.indexOf('Added house number') > -1)) {
        if (evt.type === 'afterundoaction')
            removeHNs(evt.action.houseNumber);
        else
            drawHNs([evt.action.houseNumber]);
    }
    else if (evt.action._description && (evt.action._description.indexOf('Moved house number') > -1)) {
        drawHNs([evt.action.newHouseNumber]);
    }
    else if (evt.action && evt.action.houseNumber) {
        drawHNs((evt.action.newHouseNumber ? [evt.action.newHouseNumber] : [evt.action.houseNumber]));
        setMarkersEvents();
    }
    checkMarkersEvents();
}

async function reloadClicked() {
    if (preventProcess() || ($('div.item-icon.w-icon.w-icon-refresh').attr('class').indexOf('disabled') > 0))
        return;
    await destroyAllHNs();
    processSegs('reload', W.model.segments.getByAttributes({ hasHNs: true }));
}

function initBackgroundTasks(status) {
    if (status === 'enable') {
        _HNLayerObserver = new MutationObserver(mutationsList => {
            mutationsList.forEach(() => {
                const input = $('div.olLayerDiv.house-numbers-layer div.house-number div.content.active:not(".new") input.number');
                if (input.val() === '')
                    input[0].addEventListener('change', setMarkersEvents);
            });
        });
        _saveButtonObserver = new MutationObserver(mutationsList => {
            if (mutationsList.filter(
                mutation => (mutation.attributeName === 'class')
                    && (mutation.target.classList.contains('waze-icon-save'))
                    && (mutation.oldValue.indexOf('ItemDisabled') === -1)
                    && (mutation.target.classList.contains('ItemDisabled'))
            ).length > 0) {
                if (W.editingMediator.attributes.editingHouseNumbers)
                    processSegs('afterSave', W.model.segments.getByIds(_segmentsToProcess), true);
                else
                    processSegmentsToRemove();
            }
        });
        _saveButtonObserver.observe($('#edit-buttons .waze-icon-save')[0], {
            childList: false, attributes: true, attributeOldValue: true, characterData: false, characterDataOldValue: false, subtree: false
        });
        _saveButtonObserver.observing = true;
        W.accelerators.events.on({ reloadData: destroyAllHNs });
        $('#overlay-buttons, #edit-buttons').on('click', 'div.reload-button-region', reloadClicked);
        W.model.segments.on('objectsadded', segmentsEvent, { action: 'objectsadded' });
        W.model.segments.on('objectsremoved', segmentsEvent, { action: 'objectsremoved' });
        W.model.segments.on('objectssynced', segmentsEvent, { action: 'objectssynced' });
        W.model.segments.on('objects-state-deleted', segmentsEvent, { action: 'objects-state-deleted' });
        W.model.segments.on('objectschanged-id', segmentsEvent, { action: 'objectschanged-id' });
        W.model.segmentHouseNumbers.on({
            objectsadded: objectsAddedHNs,
            objectschanged: objectsChangedHNs,
            'objectschanged-id': objectsChangedIdHNs,
            'objects-state-deleted': objectsStateDeletedHNs
        });
        W.editingMediator.on({ 'change:editingHouseNumbers': observeHNLayer });
        W.map.events.on({
            zoomend: zoomEndEvent, addlayer: checkLayerIndex, removelayer: checkLayerIndex
        });
        WazeWrap.Events.register('afterundoaction', this, afterActionsEvent);
        WazeWrap.Events.register('afteraction', this, afterActionsEvent);
        WazeWrap.Events.register('afterclearactions', this, afterActionsEvent);
        /* 2020.07.16.01 - See note at top
        _hnMouseoverCtrl.activate();
        */
        _scriptActive = true;
    }
    else if (status === 'disable') {
        _HNLayerObserver = undefined;
        _saveButtonObserver = undefined;
        W.accelerators.events.on('reloadData', null, destroyAllHNs);
        $('#overlay-buttons, #edit-buttons').off('click', 'div.reload-button-region', reloadClicked);
        W.model.segments.off('objectsadded', segmentsEvent, { action: 'objectsadded' });
        W.model.segments.off('objectsremoved', segmentsEvent, { action: 'objectsremoved' });
        W.model.segments.off('objectschanged', segmentsEvent, { action: 'objectschanged' });
        W.model.segments.off('objects-state-deleted', segmentsEvent, { action: 'objects-state-deleted' });
        W.model.segments.off('objectschanged-id', segmentsEvent, { action: 'objectschanged-id' });
        W.model.segmentHouseNumbers.off({
            objectsadded: objectsAddedHNs,
            objectschanged: objectsChangedHNs,
            'objectschanged-id': objectsChangedIdHNs,
            'objects-state-deleted': objectsStateDeletedHNs,
            objectsremoved: removeHNs
        });
        W.editingMediator.off({ 'change:editingHouseNumbers': observeHNLayer });
        W.map.events.unregister('zoomend', null, zoomEndEvent);
        W.map.events.unregister('addlayer', null, checkLayerIndex);
        W.map.events.unregister('removelayer', null, checkLayerIndex);
        WazeWrap.Events.unregister('afterundoaction', this, afterActionsEvent);
        WazeWrap.Events.unregister('afteraction', this, afterActionsEvent);
        /* 2020.07.16.01 - See note at top
        _hnMouseoverCtrl.deactivate();
        */
        _scriptActive = false;
    }
    return Promise.resolve();
}

function enterHNEditMode(evt) {
    if (evt && evt.data && evt.data.segment) {
        if (evt.data.moveMap)
            W.map.setCenter(new OpenLayers.LonLat(evt.data.segment.getCenter().x, evt.data.segment.getCenter().y), W.map.getZoom());
        W.selectionManager.setSelectedModels(evt.data.segment);
        $('#segment-edit-general .edit-house-numbers').click();
    }
}

function showTooltip(evt) {
    if ((W.map.getZoom() < 16) || W.editingMediator.attributes.editingHouseNumbers || !_settings.enableTooltip)
        return;
    if (evt && evt.object && evt.object.featureId) {
    /* 2020.07.16.01 - See note at top
    if (evt && evt.feature && evt.feature.attributes && evt.feature.attributes.featureId) {
    */
        checkTooltip();
        /* 2020.07.16.01 - See note at top
        const featureArr = evt.feature.attributes.featureId.split('|'),
        */
        const { segmentId, hnNumber } = evt.object;
        if (_popup.inUse && (_popup.hnNumber === hnNumber) && (_popup.segmentId === segmentId))
            return;
        const segment = W.model.segments.getObjectById(segmentId),
            street = W.model.streets.getObjectById(segment.attributes.primaryStreetID),
            popupPixel = W.map.getPixelFromLonLat(evt.object.lonlat),
            /* 2020.07.16.01 - See note at top
            popupPixel = W.map.getPixelFromLonLat(new OpenLayers.LonLat(evt.feature.geometry.getCentroid().x, evt.feature.geometry.getCentroid().y)),
            */
            htmlOut = ''
                + '<div class="tippy-tooltip light-border-theme" id="hnNavPointsTooltipDiv-tooltip" data-size="large" data-animation="shift-away" data-state="visible"'
                + '     data-interactive="" style="transition-duration:325ms; top:0px;">'
                + ' <div class="tippy-arrow" id="hnNavPointsTooltipDiv-arrow" style="left:83px;"></div>'
                + ' <div class="tippy-content" id="hnNavPointsTooltipDiv-content" data-state="visible" style="transition-duration: 325ms;">'
                + '     <div>'
                + '         <div class="house-number-marker-tooltip">'
                + `             <div class="title" dir="auto">${hnNumber} ${(street ? street.name : '')}</div>`
                + `             <div class="edit-button fa fa-pencil" id="hnNavPointsTooltipDiv-edit" ${(segment.canEditHouseNumbers() ? '' : ' style="display:none"')}></div>`
                + '         </div>'
                + '     </div>'
                + ' </div>'
                + '</div>';
        _$hnNavPointsTooltipDiv.html(htmlOut);
        popupPixel.origX = popupPixel.x;
        const popupWidthHalf = (_$hnNavPointsTooltipDiv.width() / 2);
        let arrowOffset = (popupWidthHalf - 15),
            xPlacement = 'top',
            moveMap = false;
        popupPixel.x = ((popupPixel.x - popupWidthHalf) > 0) ? (popupPixel.x - popupWidthHalf) : 10;
        if (popupPixel.x === 10)
            arrowOffset = popupPixel.origX - 22;
        if ((popupPixel.x + (popupWidthHalf * 2)) > $('#map')[0].clientWidth) {
            popupPixel.x = (popupPixel.origX - _$hnNavPointsTooltipDiv.width() + 8);
            arrowOffset = (_$hnNavPointsTooltipDiv.width() - 30);
            moveMap = true;
        }
        if (popupPixel.y - _$hnNavPointsTooltipDiv.height() < 0) {
            popupPixel.y += 10;
            xPlacement = 'bottom';
        }
        else {
            popupPixel.y -= (_$hnNavPointsTooltipDiv.height() + 4);
        }
        $('#hnNavPointsTooltipDiv-edit').on('click', { segment, moveMap }, enterHNEditMode);
        _$hnNavPointsTooltipDiv.css({ transform: `translate3d(${Math.round(popupPixel.x)}px, ${Math.round(popupPixel.y)}px, 0px)` });
        $('#hnNavPointsTooltipDiv-arrow').css('left', Math.round(arrowOffset));
        _$hnNavPointsTooltipDiv.attr('x-placement', xPlacement);
        _$hnNavPointsTooltipDiv.css({ visibility: 'visible' });
        _popup = { segmentId, hn_number: hnNumber, inUse: true };
    }
}

function hideTooltip() {
    checkTimeout({ timeout: 'hideTooltip' });
    _$hnNavPointsTooltipDiv.css({ visibility: 'hidden' });
    _$hnNavPointsTooltipDiv.html('');
    _popup = { segmentId: -1, hnNumber: -1, inUse: false };
}

function hideTooltipDelay(evt) {
    if (!evt)
        return;
    checkTimeout({ timeout: 'hideTooltip' });
    const parentsArr = (evt.toElement && evt.toElement.offsetParent) ? [evt.toElement.offsetParent, evt.toElement.offsetParent.offSetParent] : [];
    if (evt.toElement && ((parentsArr.indexOf(_HNNavPointsNumbersLayer.div) > -1) || (parentsArr.indexOf(_$hnNavPointsTooltipDiv[0]) > -1)))
        return;
    _timeouts.hideTooltip = window.setTimeout(hideTooltip, 100, evt);
}

function checkTooltip() {
    checkTimeout({ timeout: 'hideTooltip' });
}

function checkLayerIndex() {
    const layerIdx = W.map.layers.map(a => a.uniqueName).indexOf('__HNNavPointsNumbersLayer');
    let properIdx;
    if (_settings.keepHNLayerOnTop) {
        const layersIndexes = [],
            layersLoaded = W.map.layers.map(a => a.uniqueName);
        ['wmeGISLayersDefault', '__HNNavPointsLayer'].forEach(layerUniqueName => {
            if (layersLoaded.indexOf(layerUniqueName) > 0)
                layersIndexes.push(layersLoaded.indexOf(layerUniqueName));
        });
        properIdx = (Math.max(...layersIndexes) + 1);
    }
    else {
        properIdx = (W.map.layers.map(a => a.uniqueName).indexOf('__HNNavPointsLayer') + 1);
    }
    if (layerIdx !== properIdx) {
        W.map.layers.splice(properIdx, 0, W.map.layers.splice(layerIdx, 1)[0]);
        W.map.getOLMap().resetLayersZIndex();
    }
}

async function init() {
    const navPointsNumbersLayersOptions = {
        displayInLayerSwitcher: true,
        uniqueName: '__HNNavPointsNumbersLayer',
        selectable: true,
        labelSelect: true,
        rendererOptions: { zIndexing: true },
        styleMap: new OpenLayers.StyleMap({
            default: new OpenLayers.Style({
                strokeColor: '${Color}',
                strokeOpacity: 1,
                strokeWidth: 3,
                fillColor: '${Color}',
                fillOpacity: 0.5,
                pointerEvents: 'visiblePainted',
                label: '${hn_number}',
                fontSize: '12px',
                fontFamily: 'Rubik, Boing-light, sans-serif;',
                fontWeight: 'bold',
                direction: '${textDir}',
                labelOutlineColor: '${Color}',
                labelOutlineWidth: 3,
                labelSelect: true
            })
        })
    };
    log('Initializing.');
    await loadSettingsFromStorage();
    WazeWrap.Interface.AddLayerCheckbox('display', 'HN NavPoints', _settings.hnLines, hnLayerToggled);
    WazeWrap.Interface.AddLayerCheckbox('display', 'HN NavPoints Numbers', _settings.hnNumbers, hnNumbersLayerToggled);

    _HNNavPointsLayer = new OpenLayers.Layer.Vector('HN NavPoints Layer', {
        displayInLayerSwitcher: true,
        uniqueName: '__HNNavPointsLayer'
    });
    _HNNavPointsNumbersLayer = _settings.enableTooltip
        ? new OpenLayers.Layer.Markers('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions)
        : new OpenLayers.Layer.Vector('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
    W.map.addLayers([_HNNavPointsLayer, _HNNavPointsNumbersLayer]);
    _HNNavPointsLayer.setVisibility(_settings.hnLines);
    _HNNavPointsNumbersLayer.setVisibility(_settings.hnNumbers);
    /* 2020.07.16.01 - See note at top
    _hnMouseoverCtrl = new OpenLayers.Control.SelectFeature(_HNNavPointsNumbersLayer, {
        hover: true,
        highlightOnly: true,
        renderIntent: 'temporary',
        eventListeners: {
            featurehighlighted: showTooltip,
            featureunhighlighted: hideTooltipDelay
        }
    });
    W.map.addControl(_hnMouseoverCtrl);
    if (_settings.enableTooltip)
        _hnMouseoverCtrl.activate();
    */
    window.addEventListener('beforeunload', () => { checkShortcutsChanged(); }, false);
    new WazeWrap.Interface.Shortcut(
        'toggleHNNavPointsShortcut',
        'Toggle HN NavPoints layer',
        'layers',
        'layersToggleHNNavPoints',
        _settings.toggleHNNavPointsShortcut,
        () => { $('#layer-switcher-item_hn_navpoints').click(); },
        null
    ).add();
    new WazeWrap.Interface.Shortcut(
        'toggleHNNavPointsNumbersShortcut',
        'Toggle HN NavPoints Numbers layer',
        'layers',
        'layersToggleHNNavPointsNumbers',
        _settings.toggleHNNavPointsNumbersShortcut,
        () => { $('#layer-switcher-item_hn_navpoints_numbers').click(); },
        null
    ).add();
    $('#sidepanel-prefs').append(() => {
        let htmlOut = '<div style="border-bottom:1px solid black; padding-bottom:10px;';
        if ($('#sidepanel-prefs')[0].lastChild.tagName.search(/HR/gi) > -1) {
            const elmnt = $('#sidepanel-prefs')[0].lastChild;
            elmnt.style.borderTopColor = 'black';
            elmnt.style.color = 'black';
        }
        else {
            htmlOut += 'border-top:1px solid black;';
        }
        htmlOut += '"><h4>WME HN NavPoints</h4>'
            + '<div style="font-size:12px; margin-left:6px;">'
            + '<div style="margin-bottom:5px;" title="Disable NavPoints and house numbers when zoom level is less than specified number.\r\nMinimum: 16\r\nDefault: 17">'
            + `Disable when zoom level <<input type="text" id="HNNavPoints_disableBelowZoom" style="width:24px; height:20px; margin-left:4px;" value="${_settings.disableBelowZoom}"></input></div>`
            + `<input type="checkbox" style="margin-top:1px;" id="HNNavPoints_cbenableTooltip" title="Enable tooltip when mousing over house numbers."${(_settings.enableTooltip ? ' checked' : '')}>`
            + '     <label for="HNNavPoints_cbenableTooltip" style="font-weight:normal; vertical-align:top"'
            + '         title="Enable tooltip when mousing over house numbers.\r\nWarning: This may cause performance issues.">Enable tooltip</label><br>'
            + '<input type="checkbox" style="margin-top:1px;" id="HNNavPoints_cbkeepHNLayerOnTop" '
            + `title="Keep house numbers layer on top of all other layers."${(_settings.keepHNLayerOnTop ? ' checked' : '')}>`
            + '     <label for="HNNavPoints_cbenableTooltip" style="font-weight:normal; vertical-align:top" title="Keep house numbers layer on top of all other layers.">Keep HN layer on top</label>'
            + '</div>'
            + '<div style="margin:0 10px 0 10px; width:130px; text-align:center; font-size:12px; background:black; font-weight:600;">'
            + ' <div style="text-shadow:0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white,0 0 3px white;">Touched</div>'
            + ' <div style="text-shadow:0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange,0 0 3px orange;'
            + '     ">Touched forced</div>'
            + ' <div style="text-shadow:0 0 3px yellow,0 0 3px yellow,0 0 3px yellow, 0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow,0 0 3px yellow;'
            + '     ">Untouched</div>'
            + ' <div style="text-shadow:0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red,0 0 3px red;">Untouched forced</div>'
            + '</div></div>';
        return htmlOut;
    });
    $('#HNNavPoints_disableBelowZoom').on('change', function () {
        const newVal = Math.min(22, Math.max(16, parseInt(this.value)));
        if ((newVal !== _settings.disableBelowZoom) || (this.value !== newVal)) {
            if (newVal !== parseInt(this.value))
                this.value = newVal;
            _settings.disableBelowZoom = newVal;
            saveSettingsToStorage();
            if ((W.map.getZoom() < newVal) && (_settings.hnLines || _settings.hnNumbers))
                processSegs('settingChanged', null, true, 0);
            else if (_settings.hnLines || _settings.hnNumbers)
                processSegs('settingChanged', W.model.segments.getByAttributes({ hasHNs: true }), true, 0);
        }
    });
    $('input[id^="HNNavPoints_cb"]').off().on('click', function () {
        const settingName = $(this)[0].id.substr(14);
        if (settingName === 'enableTooltip') {
            if (!this.checked)
                _HNNavPointsNumbersLayer.clearMarkers();
            else
                _HNNavPointsNumbersLayer.destroyFeatures();
            W.map.removeLayer(_HNNavPointsNumbersLayer);
            if (this.checked)
                _HNNavPointsNumbersLayer = new OpenLayers.Layer.Markers('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
            else
                _HNNavPointsNumbersLayer = new OpenLayers.Layer.Vector('HN NavPoints Numbers Layer', navPointsNumbersLayersOptions);
            W.map.addLayer(_HNNavPointsNumbersLayer);
            _HNNavPointsNumbersLayer.setVisibility(_settings.hnNumbers);
        }
        _settings[settingName] = this.checked;
        if (settingName === 'keepHNLayerOnTop')
            checkLayerIndex();
        saveSettingsToStorage();
        if ((settingName === 'enableTooltip') && (W.map.getZoom() > (_settings.disableBelowZoom - 1)) && (_settings.hnLines || _settings.hnNumbers))
            processSegs('settingChanged', W.model.segments.getByAttributes({ hasHNs: true }), true, 0);
    });
    if (!_$hnNavPointsTooltipDiv) {
        $('#map').append(
            '<div id="hnNavPointsTooltipDiv" class="tippy-popper" role="tooltip" x-placement="top" style="z-index:9999; transition-duration:0ms; position:absolute;'
            + 'will-change:transform; top:0px; left:0px; visibility:none;"></div>'
        );
        _$hnNavPointsTooltipDiv = $('#hnNavPointsTooltipDiv');
        _$hnNavPointsTooltipDiv.on('mouseleave', null, hideTooltipDelay);
        _$hnNavPointsTooltipDiv.on('mouseenter', null, checkTooltip);
    }
    await initBackgroundTasks('enable');
    checkLayerIndex();
    log(`Fully initialized in ${Math.round(performance.now() - LOAD_BEGIN_TIME)} ms.`);
    showScriptInfoAlert();
    if (_scriptActive)
        processSegs('init', W.model.segments.getByAttributes({ hasHNs: true }));
    setTimeout(checkShortcutsChanged, 10000);
}

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);