WME HN NavPoints

Shows navigation points of all house numbers in WME

目前为 2023-04-19 提交的版本。查看 最新版本

// ==UserScript==
// @name            WME HN NavPoints
// @namespace       https://gf.qytechs.cn/users/166843
// @description     Shows navigation points of all house numbers in WME
// @version         2023.04.19.01
// @author          dBsooner
// @grant           GM_xmlhttpRequest
// @connect         gf.qytechs.cn
// @require         https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
// @license         GPLv3
// @match           http*://*.waze.com/*editor*
// @exclude         http*://*.waze.com/user/editor*
// @contributionURL https://github.com/WazeDev/Thank-The-Authors
// ==/UserScript==

/* global _, $, GM_info, GM_xmlhttpRequest, OpenLayers, W, WazeWrap */

/*
 * 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.
 *
 */

(function () {
    'use strict';

    // eslint-disable-next-line no-nested-ternary
    const _SCRIPT_SHORT_NAME = `HN NavPoints${(/beta/.test(GM_info.script.name) ? ' β' : /\(DEV\)/i.test(GM_info.script.name) ? ' Ω' : '')}`,
        _SCRIPT_LONG_NAME = GM_info.script.name,
        _IS_ALPHA_VERSION = /[Ω]/.test(_SCRIPT_SHORT_NAME),
        _IS_BETA_VERSION = /[β]/.test(_SCRIPT_SHORT_NAME),
        _PROD_URL = 'https://gf.qytechs.cn/scripts/390565-wme-hn-navpoints/code/WME%20HN%20NavPoints.user.js',
        _PROD_META_URL = 'https://gf.qytechs.cn/scripts/390565-wme-hn-navpoints/code/WME%20HN%20NavPoints.meta.js',
        _FORUM_URL = 'https://www.waze.com/forum/viewtopic.php?f=819&t=269397',
        _SETTINGS_STORE_NAME = 'WMEHNNavPoints',
        _BETA_URL = 'YUhSMGNITTZMeTluY21WaGMzbG1iM0pyTG05eVp5OXpZM0pwY0hSekx6TTVNRFUzTXkxM2JXVXRhRzR0Ym1GMmNHOXBiblJ6TFdKbGRHRXZZMjlrWlM5WFRVVWxNakJJVGlVeU1FNWhkbEJ2YVc1MGN5VXlNQ2hpWlhSaEtTNTFjMlZ5TG1weg==',
        _BETA_META_URL = 'YUhSMGNITTZMeTluY21WaGMzbG1iM0pyTG05eVp5OXpZM0pwY0hSekx6TTVNRFUzTXkxM2JXVXRhRzR0Ym1GMmNHOXBiblJ6TFdKbGRHRXZZMjlrWlM5WFRVVWxNakJJVGlVeU1FNWhkbEJ2YVc1MGN5VXlNQ2hpWlhSaEtTNXRaWFJoTG1weg==',
        _ALERT_UPDATE = true,
        _SCRIPT_VERSION = GM_info.script.version.toString(),
        _SCRIPT_VERSION_CHANGES = ['<b>NEW:</b> Check for updated version on load.',
            '<b>NEW:</b> Moved settings to new HN NavPoints tab.',
            '<b>CHANGE:</b> WME production now includes function from WME beta.'
        ],
        _DEBUG = /[βΩ]/.test(_SCRIPT_SHORT_NAME),
        _LOAD_BEGIN_TIME = performance.now(),
        _spinners = {
            destroyAllHNs: false,
            drawHNs: false,
            processSegs: false
        },
        _timeouts = {
            checkMarkersEvents: undefined,
            hideTooltip: undefined,
            onWmeReady: undefined,
            saveSettingsToStorage: undefined,
            setMarkersEvents: undefined,
            stripTooltipHTML: undefined
        },
        _holdFeatures = {
            hn: [],
            lines: []
        },
        dec = (s = '') => atob(atob(s));

    let _settings = {},
        _scriptActive = false,
        _HNLayerObserver,
        _saveButtonObserver,
        _HNNavPointsLayer,
        _HNNavPointsNumbersLayer,
        _wmeHnLayer,
        _processedSegments = [],
        _segmentsToProcess = [],
        _segmentsToRemove = [],
        _$hnNavPointsTooltipDiv,
        _popup = {
            inUse: false,
            hnNumber: -1,
            segmentId: -1
        },
        _lastVersionChecked = '0';

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

    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?.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_SHORT_NAME, _SCRIPT_VERSION, releaseNotes, (_IS_BETA_VERSION ? dec(_BETA_URL) : _PROD_URL).replace(/code\/.*\.js/, ''), _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]?.[obj.toIndex]) {
                window.clearTimeout(_timeouts[obj.timeout][obj.toIndex]);
                delete (_timeouts[obj.timeout][obj.toIndex]);
            }
        }
        else {
            if (_timeouts[obj.timeout])
                window.clearTimeout(_timeouts[obj.timeout]);
            _timeouts[obj.timeout] = undefined;
        }
    }

    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(force = false) {
        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] || force) {
                    _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, attributeOldValue: true
            });
            _HNLayerObserver.observing = true;
        }
        else if (_HNLayerObserver.observing) {
            _HNLayerObserver.disconnect();
            _HNLayerObserver.observing = false;
        }
        if (!_HNLayerObserver.observing) {
            W.model.segmentHouseNumbers.clear();
            const holdSegmentsToRemove = [..._segmentsToRemove];
            _segmentsToRemove = _segmentsToRemove.concat([..._segmentsToProcess]);
            processSegmentsToRemove(true);
            _segmentsToRemove = [...holdSegmentsToRemove];
            processSegs('exithousenumbers', W.model.segments.getByIds(_segmentsToProcess), true);
            _wmeHnLayer = undefined;
        }
        else {
            _segmentsToProcess = W.selectionManager.getSegmentSelection().segments.map((segment) => segment.attributes.id);
            _segmentsToRemove = [];
        }
        _saveButtonObserver.disconnect();
        _saveButtonObserver.observe(document.querySelector('#toolbar .js-save-popover-target'), {
            childList: false, attributes: true, attributeOldValue: true, characterData: false, characterDataOldValue: false, subtree: false
        });
    }

    function flushHeldFeatures() {
        if (_holdFeatures.hn.length === 0)
            return;
        if (_HNNavPointsLayer.getFeaturesByAttribute('featureId', _holdFeatures.hn[0].attributes.featureId).length === 0) {
            if (_settings.enableTooltip)
                _HNNavPointsNumbersLayer.addMarker(_holdFeatures.hn);
            else
                _HNNavPointsNumbersLayer.addFeatures(_holdFeatures.hn);
            _HNNavPointsLayer.addFeatures(_holdFeatures.lines);
        }
        _holdFeatures.hn = [];
        _holdFeatures.lines = [];
    }

    function removeHNs(objArr, holdFeatures = false) {
        let linesToRemove = [],
            hnsToRemove = [];
        if (_holdFeatures.hn.length > 0)
            flushHeldFeatures();
        objArr.forEach((hnObj) => {
            linesToRemove = linesToRemove.concat(_HNNavPointsLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id));
            if (holdFeatures)
                _holdFeatures.lines = _HNNavPointsLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id);
            if (!_settings.enableTooltip) {
                hnsToRemove = hnsToRemove.concat(_HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id));
                if (holdFeatures)
                    _holdFeatures.hn = _HNNavPointsNumbersLayer.getFeaturesByAttribute('featureId', hnObj.attributes.id);
            }
            else {
                _HNNavPointsNumbersLayer.markers.filter((a) => a.featureId === hnObj.attributes.id).forEach((marker) => {
                    if (holdFeatures)
                        _holdFeatures.hn = 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, hNumber: hnObj.getNumber(), strokeWidth: 3, Color: strokeColor, textDir
                    }));
                }
                if ((_holdFeatures.hn.length > 0) && (_holdFeatures.hn.map((a) => a.attributes.featureId).indexOf(featureId) > -1)) {
                    _holdFeatures.hn = [];
                    _holdFeatures.lines = [];
                }
            }
        }
        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?.error === undefined) && (typeof jsonData?.segmentHouseNumbers?.objects !== 'undefined'))
                    drawHNs(jsonData.segmentHouseNumbers.objects);
            };
        if ((action === 'objectsremoved')) {
            if (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?.dragging?.last)
                removeHNs([evt.object.model], true);
        }
        else if (evt.type === 'delete') {
            removeHNs([evt.object.model]);
        }
        else if (evt.type === 'mousedown') {
            if (evt.target.classList.contains('drag-handle') && evt?.data?.marker?.model)
                removeHNs([evt.data.marker.model], true);
        }
        else if (evt.type === 'mouseup') {
            if (evt.target.classList.contains('drag-handle') && (_holdFeatures.hn.length > 0))
                flushHeldFeatures();
        }
    }

    function setMarkersEvents(reclick = false, targetNode = undefined) {
        if (W.editingMediator.attributes.editingHouseNumbers) {
            checkTimeout({ timeout: 'setMarkersEvents' });
            hideTooltip();
            if (!_wmeHnLayer || (_wmeHnLayer?.markers?.length === 0)) {
                _timeouts.setMarkersEvents = window.setTimeout(setMarkersEvents, 50, reclick, targetNode);
                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 });
                $('.drag-handle', marker.icon.div.children[0]).off('mousedown', { marker }, markerEvent).on('mousedown', { marker }, markerEvent);
                $('.drag-handle', marker.icon.div.children[0]).off('mouseup', { marker }, markerEvent).on('mouseup', { marker }, markerEvent);
            });
            if (reclick) {
                const tmpNode = $('input.number', targetNode)[0];
                $(tmpNode)[0].focus();
                $(tmpNode)[0].setSelectionRange(tmpNode.selectionStart, tmpNode.selectionStart);
            }
        }
        else if (_wmeHnLayer) {
            _wmeHnLayer.markers.forEach((marker) => {
                marker.events.unregister('click:input', null, markerEvent);
                marker.events.unregister('delete', null, markerEvent);
            });
        }
    }

    // eslint-disable-next-line default-param-last
    function checkMarkersEvents(retry = false, tries = 0, reclick, targetNode) {
        checkTimeout({ timeout: 'checkMarkersEvents' });
        if (_wmeHnLayer?.markers?.length > 0) {
            if (!_wmeHnLayer.markers[0].events.listeners['click:input'].some((callbackFn) => callbackFn.func === markerEvent))
                setMarkersEvents(reclick, targetNode);
        }
        else if (retry && (tries < 50)) {
            _timeouts.checkMarkersEvents = window.setTimeout(checkMarkersEvents, 100, true, ++tries, reclick, targetNode);
        }
        else if (retry) {
            logError('Timeout (5 sec) exceeded waiting for markers to popuplate within checkMarkersEvents');
        }
    }

    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(true, 0);
    }

    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?.indexOf('Deleted house number') > -1) {
            if (evt.type === 'afterundoaction')
                drawHNs([evt.action.object]);
            else
                removeHNs([evt.action.object]);
            setMarkersEvents();
        }
        else if (evt.action?._description?.indexOf('Updated house number') > -1) {
            const tempEvt = _.cloneDeep(evt);
            if (evt.type === 'afterundoaction') {
                if (tempEvt.action.newAttributes?.number)
                    tempEvt.action.attributes.number = tempEvt.action.newAttributes.number;
            }
            else if (evt.type === 'afteraction') {
                if (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?.indexOf('Added house number') > -1) {
            if (evt.type === 'afterundoaction')
                removeHNs([evt.action.houseNumber]);
            else
                drawHNs([evt.action.houseNumber]);
        }
        else if (evt.action?._description?.indexOf('Moved house number') > -1) {
            drawHNs([evt.action.newHouseNumber]);
        }
        else if (evt.action?.houseNumber) {
            drawHNs((evt.action.newHouseNumber ? [evt.action.newHouseNumber] : [evt.action.houseNumber]));
            setMarkersEvents();
        }
        checkMarkersEvents();
    }

    async function reloadClicked() {
        if (preventProcess() || ($('div.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((mutation) => {
                    if (mutation.type === 'attributes') {
                        if ((mutation.oldValue?.indexOf('active') > -1) && (_holdFeatures.hn.length > 0) && ($('.active', _wmeHnLayer.div).length === 0))
                            flushHeldFeatures();
                        if ((mutation.oldValue?.indexOf('active') === -1) && mutation.target.classList.contains('active'))
                            checkMarkersEvents(true, 0, true, mutation.target);
                        const $input = $('div.olLayerDiv.house-numbers-layer div.house-number div.content.active:not(".new") input.number');
                        if ($input.val() === '')
                            $input.on('change', () => { setMarkersEvents(); }).select();
                    }
                });
            });
            _saveButtonObserver = new MutationObserver((mutationsList) => {
                if ((W.model.actionManager._redoStack.length === 0)
                    // 2023.04.06.01: Production save button observer mutations
                    && (mutationsList.some((mutation) => (mutation.attributeName === 'class')
                            && mutation.target.classList.contains('waze-icon-save')
                            && (mutation.oldValue.indexOf('ItemDisabled') === -1)
                            && mutation.target.classList.contains('ItemDisabled'))
                    // 2023.04.06.01: Beta save button observer mutations
                        || mutationsList.some((mutation) => ((mutation.attributeName === 'disabled')
                            && (mutation.oldValue === 'false')
                            && (mutation.target.attributes.disabled.value === 'true')))
                    )
                ) {
                    if (W.editingMediator.attributes.editingHouseNumbers)
                        processSegs('afterSave', W.model.segments.getByIds(_segmentsToProcess), true);
                    else
                        processSegmentsToRemove();
                }
            });
            _saveButtonObserver.observe(document.querySelector('#toolbar .js-save-popover-target'), {
                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);
            _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);
            _scriptActive = false;
        }
        return Promise.resolve();
    }

    function enterHNEditMode(evt) {
        if (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?.object?.featureId) {
            checkTooltip();
            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),
                htmlOut = ''
                + '<div id="hnNavPointsTooltipDiv-tooltip" class="tippy-box" data-state="hidden" tabindex="-1" data-theme="light-border" data-animation="fade" role="tooltip" data-placement="top" '
                + '    style="max-width: 350px; transition-duration:300ms;">'
                + ' <div id="hnNavPointsTooltipDiv-content" class="tippy-content" data-state="hidden" style="transition-duration: 300ms;">'
                + '     <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 id="hnNavPointsTooltipDiv-arrow" class="tippy-arrow" style="position: absolute; left: 0px;"></div>'
                + '</div>';
            _$hnNavPointsTooltipDiv.html(htmlOut);
            popupPixel.origX = popupPixel.x;
            const popupWidthHalf = (_$hnNavPointsTooltipDiv.width() / 2);
            let arrowOffset = (popupWidthHalf - 15),
                dataPlacement = 'top',
                moveMap = false;
            popupPixel.x = ((popupPixel.x - popupWidthHalf + 5) > 0) ? (popupPixel.x - popupWidthHalf + 5) : 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.children().toArray().reduce((height, elem) => height + $(elem).outerHeight(true), 0) < 0) {
                popupPixel.y += 14;
                dataPlacement = 'bottom';
            }
            else {
                popupPixel.y -= (_$hnNavPointsTooltipDiv.children().toArray().reduce((height, elem) => height + $(elem).outerHeight(true), 0) + 14);
            }
            $('#hnNavPointsTooltipDiv-edit').on('click', { segment, moveMap }, enterHNEditMode);
            _$hnNavPointsTooltipDiv.css({ transform: `translate(${Math.round(popupPixel.x)}px, ${Math.round(popupPixel.y)}px)` });
            $('#hnNavPointsTooltipDiv-arrow').css({ transform: `translate(${Math.max(0, Math.round(arrowOffset))}px, 0px)` });
            $('#hnNavPointsTooltipDiv-tooltip').attr('data-placement', dataPlacement).attr('data-state', 'visible');
            $('#hnNavPointsTooltipDiv-content').attr('data-state', 'visible');
            _popup = { segmentId, hNumber: hnNumber, inUse: true };
        }
    }

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

    function hideTooltip() {
        checkTimeout({ timeout: 'hideTooltip' });
        $('#hnNavPointsTooltipDiv-content').attr('data-state', 'hidden');
        $('#hnNavPointsTooltipDiv-tooltip').attr('data-state', 'hidden');
        _timeouts.stripTooltipHTML = window.setTimeout(stripTooltipHTML, 400);
    }

    function hideTooltipDelay(evt) {
        if (!evt)
            return;
        checkTimeout({ timeout: 'hideTooltip' });
        const parentsArr = 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();
        }
    }

    function checkHnNavpointsVersion() {
        if (_IS_ALPHA_VERSION)
            return;
        try {
            const metaUrl = _IS_BETA_VERSION ? dec(_BETA_META_URL) : _PROD_META_URL;
            GM_xmlhttpRequest({
                url: metaUrl,
                onload(res) {
                    const latestVersion = res.responseText.match(/@version\s+(.*)/)[1];
                    if ((latestVersion > _SCRIPT_VERSION) && (latestVersion > (_lastVersionChecked || '0'))) {
                        _lastVersionChecked = latestVersion;
                        WazeWrap.Alerts.info(
                            _SCRIPT_LONG_NAME,
                            `<a href="${(_IS_BETA_VERSION ? dec(_BETA_URL) : _PROD_URL)}" target = "_blank">Version ${latestVersion}</a> is available.<br>Update now to get the latest features and fixes.`,
                            true,
                            false
                        );
                    }
                },
                onerror(res) {
                    // Silently fail with an error message in the console.
                    logError('Upgrade version check:', res);
                }
            });
        }
        catch (err) {
            // Silently fail with an error message in the console.
            logError('Upgrade version check:', err);
        }
    }

    async function onWazeWrapReady() {
        log('Initializing.');
        checkHnNavpointsVersion();
        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: '${hNumber}',
                        fontSize: '12px',
                        fontFamily: 'Rubik, Boing-light, sans-serif;',
                        fontWeight: 'bold',
                        direction: '${textDir}',
                        labelOutlineColor: '${Color}',
                        labelOutlineWidth: 3,
                        labelSelect: true
                    })
                })
            },
            buildCheckBox = (id = '', label = '', checked = true, title = '', disabled = false) => `<wz-checkbox id="${id}" title="${title}"`
                + `${(disabled ? ' disabled' : '')}${(checked ? ' checked' : '')}`
                + `>${label}</wz-checkbox>`,
            buildTextBox = (id = '', label = '', value = '', placeHolder = '', maxlength = 0, autoComplete = 'off', title = '', disabled = false) => `<wz-text-input id="${id}" label="${label}"`
                + ` value=${value} placeholder="${placeHolder}" maxlength="${maxlength}" autocomplete="${autoComplete}" title="${title}"`
                + `${(disabled ? ' disabled' : '')}`
                + '></wz-text-input>',
            handleCheckboxToggle = 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);
            },
            handleTextboxChange = function () {
                const newVal = Math.min(22, Math.max(16, parseInt(this.value)));
                if ((newVal !== _settings.disableBelowZoom) || (parseInt(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);
                }
            };
        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);
        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();
        const { tabLabel, tabPane } = W.userscripts.registerSidebarTab('HN-NavPoints');
        tabLabel.innerHTML = '<i class="w-icon w-icon-location"></i>';
        tabLabel.title = _SCRIPT_SHORT_NAME;
        tabPane.innerHTML = `<h4><b>${_SCRIPT_LONG_NAME}</b></h4>`
            + `<h6 style="margin-top:0px">${_SCRIPT_VERSION}</h6>`
            + '<form class="attributes-form side-panel-section">'
            + '<div class="form-group">'
            + `${buildTextBox('HNNavPoints_disableBelowZoom', 'Disable when zoom level is (<) less than:', _settings.disableBelowZoom, '', 2, 'off', 'Disable NavPoints and house numbers when zoom level is less than specified number.\r\nMinimum: 16\r\nDefault: 17', false)}`
            + `${buildCheckBox('HNNavPoints_cbenableTooltip', 'Enable tooltip', _settings.enableTooltip, 'Enable tooltip when mousing over house numbers.\r\nWarning: This may cause performance issues.', false)}`
            + `${buildCheckBox('HNNavPoints_cbkeepHNLayerOnTop', 'Keep HN layer on top', _settings.keepHNLayerOnTop, 'Keep house numbers layer on top of all other layers.', false)}`
            + '</div>'
            + '</form>'
            + '<label class="control-label">Color Legend</label>'
            + '<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>';
        tabPane.id = 'sidepanel-hn-navpoints';
        await W.userscripts.waitForElementConnected(tabPane);
        $('#HNNavPoints_disableBelowZoom').off().on('focusout', handleTextboxChange);
        $('wz-checkbox[id^="HNNavPoints_cb"]').off().on('click', handleCheckboxToggle);
        if (!_$hnNavPointsTooltipDiv) {
            $('#map').append(
                '<div data-tippy-root id="hnNavPointsTooltipDiv" style="z-index:9999; visibility:visible; position:absolute; inset: auto auto 0px 0px; '
            + 'margin: 0px; top: 0px; left: 0px;"></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 onWmeReady(tries = 1) {
        if (typeof tries === 'object')
            tries = 1;
        checkTimeout({ timeout: 'onWmeReady' });
        if (WazeWrap?.Ready) {
            logDebug('WazeWrap is ready. Proceeding with initialization.');
            onWazeWrapReady();
        }
        else if (tries < 1000) {
            logDebug(`WazeWrap is not in Ready state. Retrying ${tries} of 1000.`);
            _timeouts.onWmeReady = window.setTimeout(onWmeReady, 200, ++tries);
        }
        else {
            logError(new Error('onWmeReady timed out waiting for WazeWrap Ready state.'));
        }
    }

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

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

    bootstrap();
}
)();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址