// ==UserScript==
// @name WME Quick HN (DaveAcincy fork)
// @description Quick House Numbers
// @version 2025.03.23.01
// @author Vinkoy (forked by DaveAcincy)
// @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/*
// @namespace https://greasyfork.org/users/166713
// @homepage https://www.waze.com/discuss/t/script-wme-quick-hn-daveacincy-fork/327021
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @grant GM.addStyle
// @grant unsafeWindow
// ==/UserScript==
/* global W */
/* global I18n */
/* global WazeWrap */
(function () {
const debug = false;
const scriptName = 'Quick HN';
const scriptId = 'wmeqhn';
let policySafeHTML;
let wazeMapObserver;
let lastHN;
let nextHNs;
let interval = 1;
let modeMultiplier = 1;
let fillnext = false;
let { autoSetHN = false, zoomKeys = false, custom = 4 } = JSON.parse(localStorage[scriptId] ?? '{}');
let wmeSDK;
unsafeWindow.SDK_INITIALIZED.then(() => {
wmeSDK = getWmeSdk({ scriptId, scriptName });
wmeSDK.Events.once({ eventName: 'wme-ready' }).then(async () => {
for (let initCount = 1; initCount <= 100; initCount++) {
if (WazeWrap?.Ready) return initialiseQHN();
else if (initCount === 1) console.log('Quick HN: Waiting for WazeWrap...');
await new Promise(r => setTimeout(r, 300));
}
console.error('Quick HN: WazeWrap loading failed. Giving up.');
});
});
function tlog(message, data = '') {
if (!debug) return;
const t = new Date;
const h = t.getHours();
const m = t.getMinutes();
const s = t.getSeconds();
const ms = `${t.getMilliseconds()}`.padStart(3, '0');
console.log(`QHN: ${h}:${m}:${s}.${ms}: ${message}`, data);
}
function createShortcut(shortcutId, description, callback, shortcutKeys) {
// SDK shortcuts for when that's fixed
// wmeSDK.Shortcuts.createShortcut({ callback, description, shortcutId, shortcutKeys });
I18n.translations[wmeSDK.Settings.getLocale().localeCode].keyboard_shortcuts.groups[scriptId].members[shortcutId] = description;
W.accelerators.addAction(shortcutId, { group: scriptId });
W.accelerators.events.register(shortcutId, null, callback);
W.accelerators._registerShortcuts({ [shortcutKeys]: shortcutId });
}
function saveQHNOptions() {
localStorage[scriptId] = JSON.stringify({ autoSetHN, zoomKeys, custom });
}
function initialiseQHN() {
if (typeof trustedTypes !== 'undefined') {
policySafeHTML = trustedTypes.createPolicy('policySafeHTML', { createHTML: innerText => innerText });
}
W.accelerators.Groups[scriptId] = { members: [] };
I18n.translations[wmeSDK.Settings.getLocale().localeCode].keyboard_shortcuts.groups[scriptId] = { description: scriptName, members: {} };
createShortcut('WME_QHN_newHN01', "Insert next sequential house number", () => addOrZoom(1), 't');
createShortcut('WME_QHN_newHN02', "Insert every 2nd house number", () => addOrZoom(2), 'r');
createShortcut('WME_QHN_newHNcustom', "Insert house number with custom interval", () => addOrZoom(custom), 'e');
for (let key = 1; key <= 10; key++)
createShortcut(`WME_QHN_newHN${key}`, `Insert house number ±${key}, or zoom to level ${key + 10}`, () => addOrZoom(key, key + 10), key % 10);
GM.addStyle('.qhn-panel { color: var(--content_p1); }');
wmeSDK.Sidebar.registerScriptTab().then(({ tabLabel, tabPane }) => {
tabLabel.id = scriptId;
tabLabel.innerText = scriptName;
tabLabel.title = `${scriptName} Settings`;
tabPane.innerHTML = ((text) => policySafeHTML ? policySafeHTML.createHTML(text) : text)(`
<div class="qhn-panel"><div><b>Quick House Numbers</b> v${GM_info.script.version}</div><br/>
<div><input type='checkbox' id='qhnAutoSetHNCheckbox' name='qhnAutoSetHNCheckbox' title="When enabled, auto set next HN updates the last HN based on the last HN moved" ${autoSetHN ? 'checked' : ''}> <label for='qhnAutoSetHNCheckbox'>Auto set next HN on moved HN</label></div>
<div><input type='checkbox' id='qhnZoomKeysCheckbox' name='qhnZoomKeysCheckbox' title="1-9 => Z11-19; 0 => Z20" ${zoomKeys ? 'checked' : ''}> <label for='qhnZoomKeysCheckbox'>Zoom Keys when no segment</label></div>
<div>Custom interval (E): <input type='number' id='qhnCustomInput' min='1' value='${custom}' style='width: 50px;'></div><br/>
<div>Mode: <button name='qhnModeToggle' id='qhnModeToggle'>Increment ↑</button></div><br/>
<div id="qhnTabPane"></div></div>`);
document.querySelector('#qhnAutoSetHNCheckbox').addEventListener('change', (e) => {
autoSetHN = e.target.checked;
saveQHNOptions();
});
document.querySelector('#qhnZoomKeysCheckbox').addEventListener('change', (e) => {
zoomKeys = e.target.checked;
saveQHNOptions();
updateTabPane();
});
document.querySelector('#qhnCustomInput').addEventListener('change', (e) => {
custom = e.target.value;
e.target.blur();
saveQHNOptions();
updateNextHNs();
});
document.querySelector('#qhnModeToggle').addEventListener('click', (e) => {
modeMultiplier *= -1;
e.target.innerHTML = (modeMultiplier > 0 ? 'Increment ↑' : 'Decrement ↓');
e.target.blur();
updateNextHNs();
});
WazeWrap.Events.register('afteraction', null, hnActionCheck);
updateNextHNs();
});
wazeMapObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.className === 'house-number is-active') {
const hnInput = node.querySelector('input');
if (hnInput) hnInput.onfocus = () => setHN();
}
}
}
});
wmeSDK.Events.on({
eventName: 'wme-selection-changed', eventHandler: () => {
if (wmeSDK.Editing.getSelection()?.objectType === 'segment')
wazeMapObserver.observe(document.querySelector('#WazeMap'), { childList: true, subtree: true });
else
wazeMapObserver.disconnect();
updateTabPane();
}
});
console.log("Quick HN: initialize complete");
}
// Watches changes for new/moved HNs and updates lastHN
function hnActionCheck() {
const lastAction = W.model.actionManager.getActions().at(-1);
const actionHN = lastAction?.houseNumber?.getAttribute('number');
if (actionHN && (lastAction.actionName === 'ADD_HOUSE_NUMBER' || (lastAction.actionName === 'MOVE_HOUSE_NUMBER' && autoSetHN))) {
tlog(`action: ${actionHN}`, lastAction.houseNumber);
lastHN = actionHN;
updateNextHNs();
}
}
function addOrZoom(newInterval, zoom) {
if (!newInterval) return;
if (wmeSDK.Editing.getSelection()?.objectType == 'segment') {
interval = Number(newInterval);
fillnext = true;
tlog('setFocus');
document.querySelector('wz-button:has(.w-icon-home)').click();
document.querySelector('wz-navigation-item[selected="false"] i.w-icon-script').click();
document.querySelector(`#${scriptId}`).click();
}
else if (zoomKeys && zoom) wmeSDK.Map.setZoomLevel( { zoomLevel: zoom } );
}
async function setHN() {
tlog('setHN');
const hnInput = document.querySelector('div.house-number.is-active input:placeholder-shown');
if (!fillnext || !hnInput) return;
fillnext = false;
// React hack: https://github.com/facebook/react/issues/11488#issuecomment-884790146
hnInput.value = nextHNs[interval][0];
hnInput._valueTracker?.setValue("");
hnInput.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise(r => setTimeout(r, 100));
hnInput.blur();
}
function updateNextHNs() {
nextHNs = {};
for (const interval of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, custom]) {
nextHNs[interval] = new Array(3);
let baseHN = lastHN ?? '0';
for (let index = 0; index < nextHNs[interval].length; index++) {
const nextParts = baseHN.match(/[0-9]+|[a-z]|[A-Z]|\S/g);
let thisInterval = interval;
for (const [index, part] of nextParts.reverse().entries()) {
if (!Number.isNaN(Number(part))) {
nextParts[index] = Math.max(1, Number(part) + (thisInterval * modeMultiplier)).toString().padStart(part.length, '0');
break;
}
if (/[a-z]/i.test(part)) {
let nextLetter = part.codePointAt(0) + ((thisInterval % 26) * modeMultiplier);
thisInterval = Math.floor(thisInterval / 26);
if ((/[a-z]/.test(part) && nextLetter > 'z'.codePointAt(0)) ||
(/[A-Z]/.test(part) && nextLetter > 'Z'.codePointAt(0))) {
nextLetter -= 26;
thisInterval++;
}
if ((/[a-z]/.test(part) && nextLetter < 'a'.codePointAt(0)) ||
(/[A-Z]/.test(part) && nextLetter < 'A'.codePointAt(0))) {
nextLetter += 26;
thisInterval++;
}
nextParts[index] = String.fromCodePoint(nextLetter);
if (!thisInterval) break;
}
}
baseHN = nextParts.reverse().join('');
nextHNs[interval][index] = baseHN;
}
}
updateTabPane();
}
function updateTabPane() {
document.querySelector('#qhnTabPane').innerHTML = lastHN ?
`<div>Last house number: <b>${lastHN}</b></div><br/><div>Press...` +
[['T', 1], ['R', 2], ['E', custom], ...[...Array(10).keys()].map(key => [(key + 1) % 10, key + 1])].reduce((list, [key, interval]) =>
`${list}<br/><b>${key}</b> ${zoomKeys && Number.isInteger(key) && wmeSDK.Editing.getSelection()?.objectType !== 'segment'
? `to zoom to level ${interval + 10}`
: `for HN${modeMultiplier > 0 ? "+" : "-"}${interval} <i>(${nextHNs[interval].join(", ")}...)</i>`}`
, '')
: "Manually set a house number to start using Quick HN";
}
})();