WME HN2RPP

Converts HouseNumbers to RPPs

// ==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();
})();