WME HN2RPP

Converts HouseNumbers to RPPs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME HN2RPP
// @version      2024.06.28.1
// @description  Converts HouseNumbers to RPPs
// @author       njs923/nicknick923
// @match        https://beta.waze.com/*editor*
// @match        https://www.waze.com/*editor*
// @exclude      https://www.waze.com/*user/*editor/*
// @require      https://greasyfork.org/scripts/38421-wme-utils-navigationpoint/code/WME%20Utils%20-%20NavigationPoint.js
// @grant        none
// @namespace    https://greasyfork.org/users/783417
// ==/UserScript==
/* global W, NavigationPoint, I18n, OpenLayers, require, $ */
(function() {
    'use strict';

    const d = window.document;
    const q = d.querySelector.bind(d);
    const qa = d.querySelectorAll.bind(d);
    const scriptName = 'hn2rpp';
    let settings = {};
    let lastDownloadTime = Date.now();
    let oldSegmentsId = [];
    let initCount = 0;
    function log(m) { console.log('%cWME HN2RPP:%c ' + m, 'color: darkcyan; font-weight: bold', 'color: dimgray; font-weight: normal'); }
    function warn(m) { console.warn('WME HN2RPP: ' + m); }
    function err(m) { console.error('WME HN2RPP: ' + m); }

    function bootstrap() {
        if (typeof W === 'object' && W.userscripts?.state.isReady) {
            onWmeReady();
        } else {
            document.addEventListener("wme-ready", onWmeReady, {
                once: true,
            });
        }
    }

    function onWmeReady()
    {
        initCount++;
        if ($)
            { init(); }
        else {
            if (initCount == 100) {
                err('jquery not loaded. Giving up.');
                return;
            }
            setTimeout(onWmeReady, 300);
        }
    }

    function init() {
        log('init');
        W.selectionManager.events.register("selectionchanged", null, onSelect);
        //W.editingMediator.on('change:editingHouseNumbers', onEditingHN);

        RegisterKeyboardShortcut(scriptName, 'HN2RPP', 'hn-to-rpp', txt('makeRPPButtonText'), makeHNRPP, '-1');
        RegisterKeyboardShortcut(scriptName, 'HN2RPP', 'hn-to-rpp-streetside', txt('makeStreetSideRPPButtonText'), makeStreetSideRPP, '-1');
        LoadKeyboardShortcuts(scriptName);

        window.addEventListener("beforeunload", function() {
            SaveKeyboardShortcuts(scriptName);
        }, false);
        initUI();
    }

    async function initUI(){
        const tabPaneContent = [
            '<b>', GM_info.script.name, '</b> ', GM_info.script.version,
            `<div class="controls"><div class="container"><label for="hn2rpp-default-lock-level">${txt('defaultLockLevel')}</label><select id="hn2rpp-default-lock-level"><option value="1">1</option>`,
            `<option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option></select></div>`,
            `<div class="container"><input type="checkbox" id="hn2rpp-no-duplicates" /><label for="hn2rpp-no-duplicates">${txt('noDuplicatesLabel')}</label></div></div>`,
        ].join('');

        var res = W.userscripts.registerSidebarTab(scriptName);
        var tabLabel = res.tabLabel;
        var tabPane = res.tabPane;
        tabLabel.innerText = "HN2RPP";
        tabLabel.title = "Convert HNs to RPPs";
        tabPane.innerHTML = tabPaneContent;
        await W.userscripts.waitForElementConnected(tabPane);

        const s = localStorage.hn2rpp;
        settings = s ? JSON.parse(s) : { noDuplicates: true, defaultLockLevel: 1 };

        const noDuplicatesInput = q('#hn2rpp-no-duplicates');
        const defaultLockLevelInput = q('#hn2rpp-default-lock-level');

        noDuplicatesInput.checked = settings.noDuplicates;
        noDuplicatesInput.addEventListener('change', updateSettings);

        defaultLockLevelInput.value = settings.defaultLockLevel;
        defaultLockLevelInput.addEventListener('change', updateSettings);

        log('UI initialized...');
    }

    function txt(id) {
        let texts = {
            makeRPPButtonText: 'HN→RPP(EP on HN)',
            makeStreetSideRPPButtonText: 'HN→RPP(EP on street)',
            makeRPPTitleText: 'Creates RPPs with entry point (EP) at HN location',
            makeStreetSideRPPTitleText: 'Creates RPPs with entry point (EP) at HN entry point',
            noDuplicatesLabel: 'No RPP duplicates',
            //delHNButtonText: "Delete HN",
            defaultLockLevel: 'Default lock level',
            defaultPlacement: 'Default Entry Point Placement to HN location'
        };

        return texts[id];
    }

    function makeStreetSideRPP() { makeRPP(true); }

    function makeHNRPP() { makeRPP(false); }

    function makeRPP(epAtStreet) {
        log('Creating RPPs from HouseNumbers')
        const features = W.selectionManager.getSelectedFeatures();

        if (!features || features.length === 0 || features[0].featureType !== "segment" || !features.some(f => f._wmeObject.attributes.hasHNs)) return;
        const segments = [];

        // collect all segments ids with HN
        features.forEach(f => {
            if (!f._wmeObject.attributes.hasHNs) return;
            segments.push(f.id);
        });
        // check the currently loaded housenumber objects
        let objHNs = W.model.segmentHouseNumbers.objects;
        let loadedSegmentsId = segments.filter(function(key) {
            if (Object.keys(objHNs).indexOf(key) >= 0) {
                return false;
            } else if (oldSegmentsId.indexOf(key) < 0 || lastDownloadTime < objHNs[key].attributes.updatedOn) {
                return true;
            } else {
                return false;
            }
        });
        // Now we must load the housenumbers from the server which have not been loaded in
        if (loadedSegmentsId.length > 0) {
            lastDownloadTime = Date.now();
            $.ajax({
                dataType: "json",
                url: getDownloadURL(),
                data: {ids: loadedSegmentsId.join(",")},
                success: function(json) {
                    if (json.error !== undefined) {
                    } else {
                        var ids = [];
                        if ("undefined" !== typeof(json.segmentHouseNumbers.objects)) {
                            for (var k = 0; k < json.segmentHouseNumbers.objects.length; k++) {
                                addRPPForHN(json.segmentHouseNumbers.objects[k], 'JSON', epAtStreet)
                            }
                        }
                    }
                }
            });
        }
        W.model.segmentHouseNumbers.getByIds(segments).forEach(num => {
            addRPPForHN(num, 'OBJECT', epAtStreet)
        });
    }

    function addRPPForHN(num, source, epAtStreet){
        const epsg900913 = new OpenLayers.Projection("EPSG:900913");
        const epsg4326 = new OpenLayers.Projection("EPSG:4326");
        const Landmark = require('Waze/Feature/Vector/Landmark');
        const AddLandmark = require('Waze/Action/AddLandmark');
        const UpdateFeatureAddress = require('Waze/Action/UpdateFeatureAddress');
        const seg = W.model.segments.getObjectById(num.segID);
        const addr = seg.getAddress(W.model).attributes;
        const hn = num.number;
        const fractionPoint = num.fractionPoint;
        const street = W.model.streets.getObjectById(seg.attributes.primaryStreetID);
        const streetName = street.attributes.name;
        const cityID = street.attributes.cityID;
        const city = W.model.cities.getObjectById(cityID);
        const stateID = city.attributes.stateID;
        const countryID = city.attributes.countryID;
        const houseNumber = hn;
        const geoJSONGeometry = W.userscripts.toGeoJSONGeometry(num.geometry);

        const newAddr = {
            emptyStreet: street.attributes.isEmpty, // TODO: fix this
            stateID,
            countryID,
            cityName: city.attributes.name,
            houseNumber,
            streetName,
            emptyCity: city.attributes.isEmpty // TODO: fix this
        };
        const residential = true;
        const categories = ['RESIDENTIAL'];
        const res = new Landmark({ geoJSONGeometry, categories, residential });
        /*if (source === 'JSON'){
            res.geometry = new OpenLayers.Geometry.Point(num.geometry.coordinates[0], num.geometry.coordinates[1]).transform(epsg4326, epsg900913);
        } else {
            res.geometry = num.geometry.clone();
        } */

        // set default lock level
        res.attributes.lockRank = settings.defaultLockLevel - 1;

        if(newAddr.emptyCity === true){
            let cityName = "";
            // If we haven't found a city name, search for a alt city name and use that
            if(addr.altStreets.length > 0){ //segment has alt names
                for(var j=0;j<seg.attributes.streetIDs.length;j++){
                    var altCity = W.model.cities.getObjectById(W.model.streets.getObjectById(seg.attributes.streetIDs[j]).attributes.cityID).attributes;

                    if(altCity.name !== null && altCity.name !== ""){
                        cityName = altCity.name;
                        break;
                    }
                }
            }
            if(cityName !== ""){
                newAddr.emptyCity = null;
                newAddr.cityName = cityName;
            }
        }

        // Setup a navigation point
        var ep;
        if (epAtStreet)
        {
            // let distanceToSegment = res.geometry.distanceTo(seg.geometry, { details: true });
            // const olPoint = offsetDistance(distanceToSegment.x1, distanceToSegment.y1, res.getOLGeometry().x, res.getOLGeometry().y).transform(epsg4326, epsg900913);
            ep = new NavigationPoint(fractionPoint);
        }
        else
        {
            ep = new NavigationPoint(geoJSONGeometry);
        }

        res.attributes.entryExitPoints.push(ep);

        if (settings.noDuplicates && hasDuplicates(res, addr, hn)) return;

        W.model.actionManager.add(new AddLandmark(res));
        W.model.actionManager.add(new UpdateFeatureAddress(res, newAddr));
    }

    function offsetDistance(x1, y1, x2, y2)
    {
        var xo = x1;
        var yo = y1;

        let dx = x2 - x1;
        let dy = y2 - y1;

        if (dx < 0) xo -= 5;
        else if (dx > 0) xo += 5;

        if (dy < 0) yo -= 5;
        else if (dy > 0) yo += 5;

        return new OpenLayers.Geometry.Point(xo, yo);
    }

    // Helper to create dom element with attributes
    function newEl(name, attrs) {
        const el = d.createElement(name);
        for (let attr in attrs) if (el[attr] !== undefined) el[attr] = attrs[attr];
        return el;
    }

    function updateSettings() {
        settings.noDuplicates = q('#hn2rpp-no-duplicates').checked;
        settings.defaultLockLevel = parseInt(q('#hn2rpp-default-lock-level').value);
        localStorage.hn2rpp = JSON.stringify(settings);
    }

    function onSelect(e) {
        const features = W.selectionManager.getSelectedFeatures();

        if (!features || features.length === 0 || features[0].featureType !== "segment" || !features.some(f => f._wmeObject.attributes.hasHNs)) return;
        let segElem = q('.segment-level-select');
        if (typeof e.tries === 'undefined') { e.tries = 1; }
        else { e.tries++; }
        if (!segElem) {
            if (e.tries > 50) {
                err('could not find segment edit element');
                return;
            }
            // log('seg elem not there yet ' + e.tries);
            setTimeout(function () {onSelect(e);}, 50);
            return;
        }

        const makeRPPBtn = newEl('wz-button', {
            innerText: txt('makeRPPButtonText'),
            title: txt('makeRPPTitleText')});

        const makeRPPStreetSideBtn = newEl('wz-button', {
            innerText: txt('makeStreetSideRPPButtonText'),
            title: txt('makeStreetSideRPPTitleText')});

        makeRPPBtn.setAttribute('color', 'secondary');
        makeRPPBtn.setAttribute('size', 'sm');
        makeRPPStreetSideBtn.setAttribute('color', 'secondary');
        makeRPPStreetSideBtn.setAttribute('size', 'sm');

        makeRPPBtn.addEventListener('click', makeHNRPP);
        makeRPPStreetSideBtn.addEventListener('click', makeStreetSideRPP);

        segElem.append(makeRPPBtn);
        segElem.append(makeRPPStreetSideBtn);

    }

    function hasDuplicates(poi, addr, hn) {
        const venues = W.model.venues.objects;
        for (let k in venues)
        {
            if (venues.hasOwnProperty(k)) {
                const otherPOI = venues[k];
                const otherAddr = otherPOI.getAddress(W.model).attributes;
                if (
                    poi.attributes.name == otherPOI.attributes.name
                    && hn == otherPOI.attributes.houseNumber
                    && poi.attributes.residential == otherPOI.attributes.residential
                    && addr.street.name == otherAddr.street.name
                    && addr.city.attributes.name == otherAddr.city.attributes.name
                    && addr.country.name == otherAddr.country.name
                ) return true; // This is duplicate
            }
        }
        return false;
    }

    function getDownloadURL(){
        let downloadURL = "https://www.waze.com";
        if (~document.URL.indexOf("https://beta.waze.com")) {
            downloadURL = "https://beta.waze.com";
        }
        downloadURL += getServer();
        return downloadURL;
    }

    function getServer(){
        return W.Config.api_base + "/HouseNumbers"
    }

    //setup keyboard shortcut's header and add a keyboard shortcuts
    function RegisterKeyboardShortcut(ScriptName, ShortcutsHeader, NewShortcut, ShortcutDescription, FunctionToCall, ShortcutKeysObj) {
        // Figure out what language we are using
        var language = I18n.currentLocale();
        //check for and add keyboard shourt group to WME
        try {
            var x = I18n.translations[language].keyboard_shortcuts.groups[ScriptName].members.length;
        } catch (e) {
            //setup keyboard shortcut's header
            W.accelerators.Groups[ScriptName] = []; //setup your shortcut group
            W.accelerators.Groups[ScriptName].members = []; //set up the members of your group
            I18n.translations[language].keyboard_shortcuts.groups[ScriptName] = []; //setup the shortcuts text
            I18n.translations[language].keyboard_shortcuts.groups[ScriptName].description = ShortcutsHeader; //Scripts header
            I18n.translations[language].keyboard_shortcuts.groups[ScriptName].members = []; //setup the shortcuts text
        }
        //check if the function we plan on calling exists
        if (FunctionToCall && (typeof FunctionToCall == "function")) {
            I18n.translations[language].keyboard_shortcuts.groups[ScriptName].members[NewShortcut] = ShortcutDescription; //shortcut's text
            W.accelerators.addAction(NewShortcut, {
                group: ScriptName
            }); //add shortcut one to the group
            //clear the short cut other wise the previous shortcut will be reset MWE seems to keep it stored
            var ClearShortcut = '-1';
            var ShortcutRegisterObj = {};
            ShortcutRegisterObj[ClearShortcut] = NewShortcut;
            W.accelerators._registerShortcuts(ShortcutRegisterObj);
            if (ShortcutKeysObj !== null) {
                //add the new shortcut
                ShortcutRegisterObj = {};
                ShortcutRegisterObj[ShortcutKeysObj] = NewShortcut;
                W.accelerators._registerShortcuts(ShortcutRegisterObj);
            }
            //listen for the shortcut to happen and run a function
            W.accelerators.events.register(NewShortcut, null, function() {
                FunctionToCall();
            });
        } else {
            alert('The function ' + FunctionToCall + ' has not been declared');
        }

    }
    //if saved load and set the shortcuts
    function LoadKeyboardShortcuts(ScriptName) {
        if (localStorage[ScriptName + 'KBS']) {
            var LoadedKBS = JSON.parse(localStorage[ScriptName + 'KBS']);
            for (var i = 0; i < LoadedKBS.length; i++) {
                W.accelerators._registerShortcuts(LoadedKBS[i]);
            }
        }
    }

    function SaveKeyboardShortcuts(ScriptName) {
        var TempToSave = [];
        for (var name in W.accelerators.Actions) {
            var TempKeys = "";
            if (W.accelerators.Actions[name].group == ScriptName) {
                if (W.accelerators.Actions[name].shortcut) {
                    if (W.accelerators.Actions[name].shortcut.altKey === true) {
                        TempKeys += 'A';
                    }
                    if (W.accelerators.Actions[name].shortcut.shiftKey === true) {
                        TempKeys += 'S';
                    }
                    if (W.accelerators.Actions[name].shortcut.ctrlKey === true) {
                        TempKeys += 'C';
                    }
                    if (TempKeys !== "") {
                        TempKeys += '+';
                    }
                    if (W.accelerators.Actions[name].shortcut.keyCode) {
                        TempKeys += W.accelerators.Actions[name].shortcut.keyCode;
                    }
                } else {
                    TempKeys = "-1";
                }
                var ShortcutRegisterObj = {};
                ShortcutRegisterObj[TempKeys] = W.accelerators.Actions[name].id;
                TempToSave[TempToSave.length] = ShortcutRegisterObj;
            }
        }
        localStorage[ScriptName + 'KBS'] = JSON.stringify(TempToSave);
    }

    bootstrap();
})();