WME HN2RPP

Converts HouseNumbers to RPPs

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

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

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

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

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