WME Quick RPP

Add RPPs with incrementing HNs.

// ==UserScript==
// @name         WME Quick RPP
// @namespace    https://github.com/WazeDev/wme-quick-rpp
// @version      0.0.2
// @description  Add RPPs with incrementing HNs.
// @author       Gavin Canon-Phratsachack (https://github.com/gncnpk)
// @match        https://beta.waze.com/*editor*
// @match        https://www.waze.com/*editor*
// @exclude      https://www.waze.com/*user/*editor/*
// @exclude      https://www.waze.com/discuss/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @grant        none
// @license      MIT
// @contributionURL https://github.com/WazeDev/Thank-The-Authors
// ==/UserScript==

(function() {
    'use strict';

    // Shortcut storage configuration
    const STORAGE_KEY = 'WME_QuickRPP_Shortcuts';

    // Modifier key mappings for WME shortcuts
    const KeyModifiers = {
        1: "C",    // Ctrl
        2: "S",    // Shift
        3: "CS",   // Ctrl+Shift
        4: "A",    // Alt
        5: "AC",   // Alt+Ctrl
        6: "AS",   // Alt+Shift
        7: "ACS"   // Alt+Ctrl+Shift
    };

    // Default shortcut definitions
    const defaultDefs = [
        {
            shortcutId: 'wme-quick-rpp-create',
            description: 'Create RPP with Incremented HN',
            callback: duplicateRPP,
            shortcutKeys: null
        },
        {
            shortcutId: 'wme-quick-rpp-toggle-bulk-add',
            description: 'Toggle Bulk Creation',
            callback: toggleBulkAdd,
            shortcutKeys: null
        }
    ];

    let sdk;
    let increment = localStorage.getItem("WQR_HN_INCREMENT") || 0;
    let lockLevel = localStorage.getItem("WQR_LOCK_LEVEL") || 1;
    let NewRPPHNElement;
    let toggleBulkAddElement;
    let isBulkAdd;

    if (localStorage.getItem("WQR_BULK_ADD_ENABLED") === 'true') {
        isBulkAdd = true;
    } else {
        isBulkAdd = false;
    }

    window.SDK_INITIALIZED.then(init);

    /**
     * Convert WME-style "modMask,keyCode" string to human-readable form
     * @param {string|null} shortcutKeys - WME format like "2,85" or null
     * @returns {string|null} - Human readable like "S+85" or null
     */
    function convertShortcutKeys(shortcutKeys) {
        // Handle null input
        if (shortcutKeys == null) {
            return null;
        }

        // If not "number,number" format, assume already human-readable
        if (!/^\d+,\d+$/.test(shortcutKeys)) {
            return shortcutKeys;
        }

        // Parse and convert WME format to human-readable
        const [maskStr, keyCode] = shortcutKeys.split(",");
        const mask = parseInt(maskStr, 10);
        const modString = KeyModifiers[mask] || "";

        return modString ? `${modString}+${keyCode}` : keyCode;
    }

    function loadStoredShortcuts() {
        let storedList = []
        try {
            storedList = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
        } catch (e) {
            console.warn('WME Quick RPP: Could not parse stored shortcuts, resetting to defaults')
            storedList = []
        }
        return storedList;
    }

    function storeShortcuts() {
        console.log("WME Quick RPP: Storing shortcuts...");

        // Get IDs of shortcuts managed by this script
        const ours = new Set(defaultDefs.map(d => d.shortcutId))

        // Filter and format shortcuts for storage
        const toStore = sdk.Shortcuts
            .getAllShortcuts()
            .filter(s => ours.has(s.shortcutId))
            .map(s => ({
                shortcutId: s.shortcutId,
                description: s.description,
                shortcutKeys: s.shortcutKeys
            }))

        // Save to localStorage
        localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore))
    }

    function init() {
        sdk = window.getWmeSdk({
            scriptId: 'wme-quick-rpp',
            scriptName: 'WME Quick RPP'
        });

        // Load stored shortcuts and register them
        const existingIds = sdk.Shortcuts
            .getAllShortcuts()
            .map(s => s.shortcutId);

        let storedList = loadStoredShortcuts();

        if (storedList.length) {
            console.log('WME Quick RPP: Importing stored shortcuts');
            for (const st of storedList) {
                const def = defaultDefs.find(d => d.shortcutId === st.shortcutId);
                if (!def) {
                    console.warn(`WME Quick RPP: Unknown shortcutId ${st.shortcutId}`);
                    continue;
                }
                const toCreate = {
                    shortcutId: def.shortcutId,
                    description: def.description,
                    callback: def.callback,
                    shortcutKeys: convertShortcutKeys(st.shortcutKeys)
                };
                if (!existingIds.includes(def.shortcutId)) {
                    sdk.Shortcuts.createShortcut(toCreate);
                }
            }
        } else {
            console.log('WME Quick RPP: No stored shortcuts, registering defaults');
            for (const def of defaultDefs) {
                if (!existingIds.includes(def.shortcutId)) {
                    sdk.Shortcuts.createShortcut(def);
                }
            }
        }

        // Set up automatic storage when shortcuts are edited
        sdk.Events.on({
            eventName: 'wme-after-edit',
            eventHandler: storeShortcuts
        });

        sdk.Sidebar.registerScriptTab().then(async (res) => {
            res.tabLabel.innerText = "Quick RPP";
            res.tabPane.innerHTML = `
                <div>
                    <label for="WQRLockLevel">Lock Level</label>
                    <input type="number" id="WQRLockLevel" value="${lockLevel}" style="width: 100%;" min="1" max="6" />
                </div>
                <div>
                    <label for="WQRHNIncrement">HN Increment</label>
                    <input type="number" id="WQRHNIncrement" value="${increment}" style="width: 100%;" />
                </div>
                <div>
                    <label for="WQRToggleBulkAdd">Toggle Bulk Creation</label>
                    <input type="checkbox" id="WQRToggleBulkAdd" value="${isBulkAdd}" />
                </div>
                <div id="NewRPPHouseNumber" style="margin-top:8px;font-weight:bold;">New RPP HN: </div>
            `;
            res.tabPane.querySelector("#WQRLockLevel").addEventListener("input", (e) => {
                lockLevel = parseInt(e.target.value);
                localStorage.setItem("WQR_LOCK_LEVEL", lockLevel);
            });
            res.tabPane.querySelector("#WQRHNIncrement").addEventListener("input", (e) => {
                increment = parseInt(e.target.value);
                localStorage.setItem("WQR_HN_INCREMENT", increment);
            });
            res.tabPane.querySelector("#WQRToggleBulkAdd").addEventListener("input", (e) => {
                isBulkAdd = e.target.checked;
                localStorage.setItem("WQR_BULK_ADD_ENABLED", isBulkAdd);
            });
            NewRPPHNElement = res.tabPane.querySelector("#NewRPPHouseNumber");
            toggleBulkAddElement = res.tabPane.querySelector("#WQRToggleBulkAdd");
            if (isBulkAdd === true) {
                toggleBulkAddElement.checked = true;
            } else {
                toggleBulkAddElement.checked = false;
            }
        });
    }

    function toggleBulkAdd() {
        if (isBulkAdd === 'true' || isBulkAdd === true) {
            console.log(`WME Quick RPP: Bulk creation mode disabled!`);
            toggleBulkAddElement.checked = false;
            isBulkAdd = false;
        } else {
            console.log(`WME Quick RPP: Bulk creation mode enabled!`);
            toggleBulkAddElement.checked = true;
            isBulkAdd = true;
        }
        localStorage.setItem("WQR_BULK_ADD_ENABLED", isBulkAdd);
    }

    async function duplicateRPP() {
        let actualLockLevel = lockLevel - 1;
        let selection = sdk.Editing.getSelection();
        if (selection === null) {
            return;
        }
        let originalRPPId = selection.ids[0];
        let originalRPP = sdk.DataModel.Venues.getById({
            "venueId": originalRPPId
        });
        if (!originalRPP.isResidential) {
            return
        }
        let originalRPPAddress = sdk.DataModel.Venues.getAddress({
            "venueId": originalRPPId
        });
        let originalRPPHouseNumber = parseInt(originalRPPAddress.houseNumber);
        let originalRPPStreetId = originalRPPAddress.street.id;
        if (isBulkAdd === 'true' || isBulkAdd === true) {
            while (isBulkAdd === 'true' || isBulkAdd === true) {
                let newRPPHouseNumber = parseInt(originalRPPHouseNumber) + parseInt(increment);
                NewRPPHNElement.innerText = (`New RPP HN: ${newRPPHouseNumber}`);
                let newRPPGeometry = await sdk.Map.drawPoint();
                let newRPPId = sdk.DataModel.Venues.addVenue({
                    category: "RESIDENTIAL",
                    geometry: newRPPGeometry
                });
                sdk.DataModel.Venues.updateAddress({
                    houseNumber: `${newRPPHouseNumber}`,
                    streetId: originalRPPStreetId,
                    venueId: `${newRPPId}`
                });
                sdk.DataModel.Venues.replaceNavigationPoints({
                    navigationPoints: [{
                        isEntry: true,
                        isPrimary: true,
                        point: newRPPGeometry
                    }],
                    venueId: `${newRPPId}`
                });
                sdk.DataModel.Venues.updateVenue({
                    venueId: `${newRPPId}`,
                    lockRank: actualLockLevel
                });
                originalRPPHouseNumber = parseInt(newRPPHouseNumber);
                sdk.Editing.setSelection({selection: {ids: [`${newRPPId}`], objectType: "venue"}});
            }
        } else {
            let newRPPHouseNumber = parseInt(originalRPPHouseNumber) + parseInt(increment);
            NewRPPHNElement.innerText = (`New RPP HN: ${newRPPHouseNumber}`);
            let newRPPGeometry = await sdk.Map.drawPoint();
            let newRPPId = sdk.DataModel.Venues.addVenue({
                category: "RESIDENTIAL",
                geometry: newRPPGeometry
            });
            sdk.DataModel.Venues.updateAddress({
                houseNumber: `${newRPPHouseNumber}`,
                streetId: originalRPPStreetId,
                venueId: `${newRPPId}`
            });
            sdk.DataModel.Venues.replaceNavigationPoints({
                navigationPoints: [{
                    isEntry: true,
                    isPrimary: true,
                    point: newRPPGeometry
                }],
                venueId: `${newRPPId}`
            });
            sdk.DataModel.Venues.updateVenue({
                venueId: `${newRPPId}`,
                lockRank: actualLockLevel
            });
            sdk.Editing.setSelection({selection: {ids: [`${newRPPId}`], objectType: "venue"}});
        }
    }
})();