// ==UserScript==
// @name WME EZRoad Mod
// @namespace https://greasyfork.org/users/1087400
// @version 2.5.9.7
// @description Easily update roads
// @author https://greasyfork.org/en/users/1087400-kid4rm90s
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @exclude https://www.waze.com/user/*editor/*
// @exclude https://www.waze.com/*/user/*editor/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant unsafeWindow
// @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @license GNU GPL(v3)
// @connect greasyfork.org
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require https://update.greasyfork.icu/scripts/509664/WME%20Utils%20-%20Bootstrap.js
// @downloadURL none
// ==/UserScript==
/*Script modified from WME EZRoad (https://greasyfork.org/en/scripts/518381-wme-ezsegments) original author: Michaelrosstarr and thanks to him*/
(function main() {
'use strict';
const updateMessage = `
2.5.9.7 - 2025-12-24
- Added custom preset system for lock/speed configurations
- Save and load multiple custom lock/speed presets with custom names
- Improved UI with compact design and better dark mode support
- Export/Import buttons reorganized for better layout
`;
const scriptName = GM_info.script.name;
const scriptVersion = GM_info.script.version;
const downloadUrl = 'https://greasyfork.org/scripts/528552-wme-ezroad-mod/code/wme-ezroad-mod.user.js';
const forumURL = 'https://greasyfork.org/scripts/528552-wme-ezroad-mod/feedback';
let wmeSDK;
const roadTypes = [
{ id: 1, name: 'Motorway', value: 3, shortcutKey: 'S+1' },
{ id: 2, name: 'Ramp', value: 4, shortcutKey: 'S+2' },
{ id: 3, name: 'Major Highway', value: 6, shortcutKey: 'S+3' },
{ id: 4, name: 'Minor Highway', value: 7, shortcutKey: 'S+4' },
{ id: 5, name: 'Primary Street', value: 2, shortcutKey: 'S+5' },
{ id: 6, name: 'Street', value: 1, shortcutKey: 'S+6' },
{ id: 7, name: 'Narrow Street', value: 22, shortcutKey: 'S+7' },
{ id: 8, name: 'Offroad', value: 8, shortcutKey: 'S+8' },
{ id: 9, name: 'Parking Road', value: 20, shortcutKey: 'S+9' },
{ id: 10, name: 'Private Road', value: 17, shortcutKey: 'S+0' },
{ id: 11, name: 'Ferry', value: 15, shortcutKey: 'A+1' },
{ id: 12, name: 'Railway', value: 18, shortcutKey: 'A+2' },
{ id: 13, name: 'Runway', value: 19, shortcutKey: 'A+3' },
{ id: 14, name: 'Foothpath', value: 5, shortcutKey: 'A+4' },
{ id: 15, name: 'Pedestrianised Area', value: 10, shortcutKey: 'A+5' },
{ id: 16, name: 'Stairway', value: 16, shortcutKey: 'A+6' },
];
const defaultOptions = {
roadType: 1,
unpaved: false,
setStreet: false,
setStreetCity: false,
setStreetState: false,
autosave: false,
setSpeed: 40,
setLock: false,
updateSpeed: false,
copySegmentName: false,
locks: roadTypes.map((roadType) => ({ id: roadType.id, lock: String(1) })),
speeds: roadTypes.map((roadType) => ({ id: roadType.id, speed: 40 })),
copySegmentAttributes: false,
shortcutKey: 'g',
};
const locks = [
{ id: 1, value: '1' },
{ id: 2, value: '2' },
{ id: 3, value: '3' },
{ id: 4, value: '4' },
{ id: 5, value: '5' },
{ id: 6, value: '6' },
{ id: 'HRCS', value: 'HRCS' },
];
const log = (message) => {
if (typeof message === 'string') {
console.log('WME_EZRoads_Mod: ' + message);
} else {
console.log('WME_EZRoads_Mod: ', message);
}
};
unsafeWindow.SDK_INITIALIZED.then(initScript);
function initScript() {
wmeSDK = getWmeSdk({
scriptId: 'wme-ez-roads-mod',
scriptName: 'EZ Roads Mod',
});
WME_EZRoads_Mod_bootstrap();
}
const getCurrentCountry = () => {
return wmeSDK.DataModel.Countries.getTopCountry();
};
const getTopCity = () => {
return wmeSDK.DataModel.Cities.getTopCity();
};
const getAllCities = () => {
return wmeSDK.DataModel.Cities.getAll();
};
// --- NEW: Helper to get all connected segment IDs ---
function getConnectedSegmentIDs(segmentId) {
// Returns unique IDs of all segments connected to the given segment
const segs = [...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }), ...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true })];
const ids = segs.map((segment) => segment.id);
// Remove duplicates
return [...new Set(ids)];
}
// --- NEW: Helper to get the first connected segment's address (recursively) ---
function getFirstConnectedSegmentAddress(segmentId) {
const nonMatches = [];
const segmentIDsToSearch = [segmentId];
const hasAddress = (id) => {
const addr = wmeSDK.DataModel.Segments.getAddress({ segmentId: id });
return addr && !addr.isEmpty;
};
while (segmentIDsToSearch.length > 0) {
const startSegmentID = segmentIDsToSearch.pop();
const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
const hasAddrSegmentId = connectedSegmentIDs.find(hasAddress);
if (hasAddrSegmentId) {
return wmeSDK.DataModel.Segments.getAddress({ segmentId: hasAddrSegmentId });
}
nonMatches.push(startSegmentID);
connectedSegmentIDs.forEach((segmentID) => {
if (!nonMatches.includes(segmentID) && !segmentIDsToSearch.includes(segmentID)) {
segmentIDsToSearch.push(segmentID);
}
});
}
return null;
}
const saveOptions = (options) => {
window.localStorage.setItem('WME_EZRoads_Mod_Options', JSON.stringify(options));
};
const getOptions = () => {
const savedOptions = JSON.parse(window.localStorage.getItem('WME_EZRoads_Mod_Options')) || {};
// Deep merge for locks and speeds arrays
const mergeById = (defaults, saved, key) => {
if (!Array.isArray(defaults)) return defaults;
if (!Array.isArray(saved)) return defaults;
return defaults.map((def) => {
const found = saved.find((s) => s.id === def.id);
return found ? { ...def, ...found } : def;
});
};
const mergedLocks = mergeById(
defaultOptions.locks,
(savedOptions.locks || []).map((l) => ({ ...l, lock: String(l.lock) })),
'locks'
);
const mergedSpeeds = mergeById(defaultOptions.speeds, savedOptions.speeds || [], 'speeds');
return {
...defaultOptions,
...savedOptions,
locks: mergedLocks,
speeds: mergedSpeeds,
};
};
const saveCustomPreset = (presetName) => {
const options = getOptions();
const presets = getCustomPresets();
presets[presetName] = {
locks: options.locks,
speeds: options.speeds,
savedAt: new Date().toISOString(),
};
window.localStorage.setItem('WME_EZRoads_Mod_CustomPresets', JSON.stringify(presets));
return true;
};
const loadCustomPreset = (presetName) => {
const presets = getCustomPresets();
if (!presets[presetName]) return false;
const options = getOptions();
options.locks = presets[presetName].locks;
options.speeds = presets[presetName].speeds;
saveOptions(options);
return true;
};
const deleteCustomPreset = (presetName) => {
const presets = getCustomPresets();
if (!presets[presetName]) return false;
delete presets[presetName];
window.localStorage.setItem('WME_EZRoads_Mod_CustomPresets', JSON.stringify(presets));
return true;
};
const getCustomPresets = () => {
const presets = JSON.parse(window.localStorage.getItem('WME_EZRoads_Mod_CustomPresets')) || {};
return presets;
};
const WME_EZRoads_Mod_bootstrap = () => {
if (!document.getElementById('edit-panel') || !wmeSDK.DataModel.Countries.getTopCountry()) {
setTimeout(WME_EZRoads_Mod_bootstrap, 250);
return;
}
if (wmeSDK.State.isReady) {
WME_EZRoads_Mod_init();
} else {
wmeSDK.Events.once({ eventName: 'wme-ready' }).then(WME_EZRoads_Mod_init());
}
};
let openPanel;
const WME_EZRoads_Mod_init = () => {
log('Initing');
const options = getOptions();
const shortcutId = 'EZRoad_Mod_QuickUpdate';
// Only register if not already present
if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) {
registerShortcut(options.shortcutKey || 'g');
}
// --- ENHANCED: Add event listeners to each road-type chip for direct click handling ---
// Global flag to suppress attribute copy when chip is clicked
window.suppressCopySegmentAttributes = false;
function addRoadTypeChipListeners() {
const chipSelect = document.querySelector('.road-type-chip-select');
if (!chipSelect) return;
const chips = chipSelect.querySelectorAll('wz-checkable-chip');
chips.forEach((chip) => {
if (!chip._ezroadmod_listener) {
chip._ezroadmod_listener = true;
chip.addEventListener('click', function () {
// Log every chip click for debugging
log('Chip clicked: value=' + chip.getAttribute('value') + ', checked=' + chip.getAttribute('checked'));
setTimeout(() => {
// Only act if this chip is now the selected one (checked="")
if (chip.getAttribute('checked') === '') {
const rtValue = parseInt(chip.getAttribute('value'), 10);
log('Detected chip selection, applying EZRoadMod logic for roadType value: ' + rtValue);
if (isNaN(rtValue)) return;
const options = getOptions();
options.roadType = rtValue;
saveOptions(options);
if (typeof updateRoadTypeRadios === 'function') {
updateRoadTypeRadios(rtValue);
}
const selection = wmeSDK.Editing.getSelection();
if (selection && selection.objectType === 'segment') {
wmeSDK.Editing.setSelection({ selection });
}
setTimeout(() => {
log('Calling handleUpdate() after chip click for roadType value: ' + rtValue);
window.suppressCopySegmentAttributes = true;
Promise.resolve(handleUpdate()).finally(() => {
window.suppressCopySegmentAttributes = false;
});
}, 100);
}
}, 50);
});
}
});
}
// Call after panel is available and after any UI changes that might re-render the chips
setTimeout(addRoadTypeChipListeners, 1200);
// Also call after every edit panel mutation to re-attach listeners
// Observe the edit panel for segment changes and add the quick update button
const roadObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
for (let i = 0; i < mutation.addedNodes.length; i++) {
const addedNode = mutation.addedNodes[i];
if (addedNode.nodeType === Node.ELEMENT_NODE) {
let editSegment = addedNode.querySelector('#segment-edit-general');
if (editSegment) {
openPanel = editSegment;
const parentElement = editSegment.parentNode;
if (!parentElement.querySelector('[data-ez-roadmod-button="true"]')) {
log('Creating Quick Set Road button for this panel');
const quickButton = document.createElement('wz-button');
quickButton.setAttribute('type', 'button');
quickButton.setAttribute('style', 'margin-bottom: 5px; width: 100%');
quickButton.setAttribute('disabled', 'false');
quickButton.setAttribute('data-ez-roadmod-button', 'true');
quickButton.setAttribute('id', 'ez-roadmod-quick-button-' + Date.now()); // Unique ID using timestamp
quickButton.classList.add('send-button', 'ez-comment-button');
quickButton.textContent = 'Quick Update Segment';
parentElement.insertBefore(quickButton, editSegment);
quickButton.addEventListener('mousedown', () => handleUpdate());
log('Button created for current panel');
} else {
log('This panel already has the button, skipping creation');
}
// Always re-attach chip listeners after panel mutation
addRoadTypeChipListeners();
}
}
}
});
});
roadObserver.observe(document.getElementById('edit-panel'), {
childList: true,
subtree: true,
});
constructSettings();
function updateRoadTypeRadios(newValue) {
$(`input[name="defaultRoad"]`).each(function () {
if (parseInt($(this).attr('data-road-value'), 10) === newValue) {
$(this).prop('checked', true);
} else {
$(this).prop('checked', false);
}
});
}
// Register shortcut for each road type (move here, after handleUpdate is defined)
roadTypes.forEach((rt) => {
const shortcutId = `EZRoad_Mod_SelectRoadType_${rt.id}`;
// Prevent duplicate shortcut registration
if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) {
try {
wmeSDK.Shortcuts.createShortcut({
callback: () => {
const options = getOptions();
options.roadType = rt.value;
saveOptions(options);
updateRoadTypeRadios(rt.value);
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.success('EZRoads Mod', `Selected road type: ${rt.name}`, false, false, 1500);
}
},
description: `Select road type: ${rt.name}`,
shortcutId,
shortcutKeys: rt.shortcutKey,
});
} catch (e) {
log(`Shortcut registration failed for ${rt.name}: ${e}`);
}
}
});
log('Completed Init');
};
// Helper to register the shortcut, avoids duplicate code
function registerShortcut(shortcutKey) {
if (!wmeSDK?.Shortcuts) return;
const shortcutId = 'EZRoad_Mod_QuickUpdate';
// Always delete before creating to avoid duplicates
if (wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) {
wmeSDK.Shortcuts.deleteShortcut({ shortcutId });
}
try {
wmeSDK.Shortcuts.createShortcut({
callback: handleUpdate,
description: 'Quick Update Segments.',
shortcutId,
shortcutKeys: shortcutKey,
});
console.log(`[EZRoads Mod] Shortcut '${shortcutKey}' for Quick Update Segments enabled.`);
} catch (e) {
// If shortcut registration fails (e.g., conflict), register with no key so it appears in WME UI
console.warn('[EZRoads Mod] Shortcut registration failed:', e);
try {
wmeSDK.Shortcuts.createShortcut({
callback: handleUpdate,
description: 'Quick Update Segments.',
shortcutId,
shortcutKeys: null, // Register with no key so it appears in WME UI
});
console.log('[EZRoads Mod] Registered shortcut with no key due to conflict.');
} catch (e2) {
console.error('[EZRoads Mod] Failed to register shortcut with no key:', e2);
}
const options = getOptions();
options.shortcutKey = null;
saveOptions(options);
}
}
const getEmptyCity = () => {
return (
wmeSDK.DataModel.Cities.getCity({
cityName: '',
countryId: getCurrentCountry().id,
}) ||
wmeSDK.DataModel.Cities.addCity({
cityName: '',
countryId: getCurrentCountry().id,
})
);
};
const delayedUpdate = (updateFn, delay) => {
return new Promise((resolve) => {
setTimeout(() => {
updateFn();
resolve();
}, delay);
});
};
function getHighestSegLock(segID) {
const segObj = wmeSDK.DataModel.Segments.getById({ segmentId: segID });
if (!segObj) {
console.warn(`Segment object with ID ${segID} not found in DataModel.Segments.`);
return 1; // Default lock level if segment not found
}
const segType = segObj.roadType;
const checkedSegs = [];
let forwardLock = null;
let reverseLock = null;
function processForNode(forwardID) {
checkedSegs.push(forwardID);
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: forwardID });
if (!seg) return forwardLock;
const forNodeId = seg.toNodeId;
if (!forNodeId) return forwardLock;
// Get all segments connected to this node
const allSegs = wmeSDK.DataModel.Segments.getAll();
const forNodeSegs = allSegs.filter((s) => s.fromNodeId === forNodeId || s.toNodeId === forNodeId).map((s) => s.id);
// Remove the current segment from the list
const filteredSegs = forNodeSegs.filter((id) => id !== forwardID);
for (let i = 0; i < filteredSegs.length; i++) {
const conSegObj = wmeSDK.DataModel.Segments.getById({
segmentId: filteredSegs[i],
});
if (!conSegObj) continue;
if (conSegObj.roadType !== segType) {
forwardLock = Math.max(conSegObj.lockRank ?? 0, forwardLock ?? 0);
} else {
if (!checkedSegs.includes(conSegObj.id)) {
const tempRank = processForNode(conSegObj.id);
forwardLock = Math.max(tempRank ?? 0, forwardLock ?? 0);
}
}
}
return forwardLock ?? 0;
}
function processRevNode(reverseID) {
checkedSegs.push(reverseID);
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: reverseID });
if (!seg) return reverseLock;
const revNodeId = seg.fromNodeId;
if (!revNodeId) return reverseLock;
// Get all segments connected to this node
const allSegs = wmeSDK.DataModel.Segments.getAll();
const revNodeSegs = allSegs.filter((s) => s.fromNodeId === revNodeId || s.toNodeId === revNodeId).map((s) => s.id);
// Remove the current segment from the list
const filteredSegs = revNodeSegs.filter((id) => id !== reverseID);
for (let i = 0; i < filteredSegs.length; i++) {
const conSegObj = wmeSDK.DataModel.Segments.getById({
segmentId: filteredSegs[i],
});
if (!conSegObj) continue;
if (conSegObj.roadType !== segType) {
reverseLock = Math.max(conSegObj.lockRank ?? 0, reverseLock ?? 0);
} else {
if (!checkedSegs.includes(conSegObj.id)) {
const tempRank = processRevNode(conSegObj.id);
reverseLock = Math.max(tempRank ?? 0, reverseLock ?? 0);
}
}
}
return reverseLock ?? 0;
}
let calculatedLock = Math.max(processForNode(segID), processRevNode(segID));
return Math.min(calculatedLock, 6); // Limit to L6
}
function pushCityNameAlert(cityId, alertMessageParts) {
let cityName = '';
if (cityId) {
const city = wmeSDK.DataModel.Cities.getById({ cityId });
cityName = city && city.name ? city.name : '';
}
alertMessageParts.push(`City Name: ${cityName || 'None'}`);
}
// Helper: Returns true if the roadType is Footpath, Pedestrianised Area, or Stairway
function isPedestrianType(roadType) {
return [5, 10, 16].includes(roadType);
}
// Helper: If switching between pedestrian and non-pedestrian types, delete and recreate the segment
function recreateSegmentIfNeeded(segmentId, targetRoadType, copyConnectedNameData) {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId });
if (!seg) return segmentId;
const currentIsPed = isPedestrianType(seg.roadType);
const targetIsPed = isPedestrianType(targetRoadType);
if (currentIsPed !== targetIsPed) {
// Show confirmation dialog before swapping
let swapMsg = currentIsPed
? 'You are about to convert a Pedestrian type segment (Footpath, Pedestrianised Area, or Stairway) to a regular street type. This will delete and recreate the segment. Continue?'
: 'You are about to convert a regular street segment to a Pedestrian type (Footpath, Pedestrianised Area, or Stairway). This will delete and recreate the segment. Continue?';
if (!window.confirm(swapMsg)) {
return null; // Cancel operation
}
// Save geometry and address
const geometry = seg.geometry;
const oldPrimaryStreetId = seg.primaryStreetId;
const oldAltStreetIds = seg.alternateStreetIds;
try {
wmeSDK.DataModel.Segments.deleteSegment({ segmentId });
} catch (ex) {
if (ex instanceof wmeSDK.Errors.InvalidStateError) {
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.error('EZRoads Mod Beta', 'Segment could not be deleted. Please check for restrictions or junctions.');
}
return null;
}
}
// Create new segment
const newSegmentId = wmeSDK.DataModel.Segments.addSegment({ geometry, roadType: targetRoadType });
// Ensure primaryStreetId is valid (not null or undefined)
let validPrimaryStreetId = oldPrimaryStreetId;
if (!validPrimaryStreetId) {
// Use a blank street in the current city
let segCityId = getTopCity()?.id;
if (!segCityId) {
// fallback to country if city is not available
segCityId = getCurrentCountry()?.id;
}
let blankStreet = wmeSDK.DataModel.Streets.getStreet({
cityId: segCityId,
streetName: '',
});
if (!blankStreet) {
blankStreet = wmeSDK.DataModel.Streets.addStreet({
streetName: '',
cityId: segCityId,
});
}
validPrimaryStreetId = blankStreet.id;
}
// Restore address with valid primaryStreetId
wmeSDK.DataModel.Segments.updateAddress({
segmentId: newSegmentId,
primaryStreetId: validPrimaryStreetId,
alternateStreetIds: oldAltStreetIds,
});
// If we have connected segment name data to copy, apply it now
if (copyConnectedNameData && copyConnectedNameData.primaryStreetId) {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: newSegmentId,
primaryStreetId: copyConnectedNameData.primaryStreetId,
alternateStreetIds: copyConnectedNameData.alternateStreetIds || [],
});
}
// Reselect new segment
wmeSDK.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } });
return newSegmentId;
}
return segmentId;
}
const handleUpdate = () => {
const selection = wmeSDK.Editing.getSelection();
if (!selection || selection.objectType !== 'segment') return;
log('Updating RoadType');
const options = getOptions();
let alertMessageParts = [];
let updatedRoadType = false;
let updatedLockLevel = false;
let updatedSpeedLimit = false;
let updatedPaved = false;
let updatedCityName = false;
let updatedSegmentName = false;
const updatePromises = [];
// If copySegmentAttributes is checked, copy all attributes from a connected segment
if (options.copySegmentAttributes && !window.suppressCopySegmentAttributes) {
selection.ids.forEach((id) => {
updatePromises.push(
delayedUpdate(() => {
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const fromNode = seg.fromNodeId;
const connectedSegIds = getConnectedSegmentIDs(id);
// Gather all segments connected to fromNode (excluding self)
const fromNodeSegs = connectedSegIds.map((sid) => wmeSDK.DataModel.Segments.getById({ segmentId: sid })).filter((s) => s && (s.fromNodeId === fromNode || s.toNodeId === fromNode) && s.id !== id);
// Prefer the first fromNode segment with a valid primary street name (and optionally other attributes)
let preferredSeg = fromNodeSegs.find((s) => {
if (!s) return false;
const street = wmeSDK.DataModel.Streets.getById({ streetId: s.primaryStreetId });
return street && street.name;
});
let segsToTry = [];
if (preferredSeg) {
segsToTry.push(preferredSeg.id);
// Add the rest, excluding preferredSeg.id
segsToTry = segsToTry.concat(connectedSegIds.filter((cid) => cid !== preferredSeg.id));
} else {
segsToTry = connectedSegIds;
}
let found = false;
for (let connectedSegId of segsToTry) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
if (!connectedSeg) continue;
const street = wmeSDK.DataModel.Streets.getById({ streetId: connectedSeg.primaryStreetId });
if (street && street.name) {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: connectedSeg.fwdSpeedLimit,
revSpeedLimit: connectedSeg.revSpeedLimit,
roadType: connectedSeg.roadType,
lockRank: connectedSeg.lockRank,
});
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
// Copy paved/unpaved
const isUnpaved = connectedSeg.flagAttributes && connectedSeg.flagAttributes.unpaved === true;
let toggled = false;
const segPanel = openPanel;
if (segPanel) {
const unpavedIcon = segPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
if (isUnpaved !== (seg.flagAttributes && seg.flagAttributes.unpaved === true)) {
unpavedChip.click();
toggled = true;
}
}
}
if (!toggled) {
try {
const wzCheckbox = segPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked !== isUnpaved) {
hiddenInput.click();
toggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode unpaved toggle method failed: ' + e);
}
}
}
alertMessageParts.push(`Copied all attributes from connected segment.`);
found = true;
break;
}
}
// If no connected segment with valid street name was found, fallback to any connected segment (like the other logic)
if (!found) {
let fallbackSegId = null;
const segObj = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const fromNode = segObj.fromNodeId;
const toNode = segObj.toNodeId;
const allSegs = wmeSDK.DataModel.Segments.getAll();
for (let s of allSegs) {
if (s.id !== id && (s.fromNodeId === fromNode || s.toNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === toNode)) {
fallbackSegId = s.id;
break;
}
}
if (fallbackSegId) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: fallbackSegId });
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: connectedSeg.fwdSpeedLimit,
revSpeedLimit: connectedSeg.revSpeedLimit,
roadType: connectedSeg.roadType,
lockRank: connectedSeg.lockRank,
});
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
// Copy paved/unpaved
const isUnpaved = connectedSeg.flagAttributes && connectedSeg.flagAttributes.unpaved === true;
let toggled = false;
const segPanel = openPanel;
if (segPanel) {
const unpavedIcon = segPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
if (isUnpaved !== (seg.flagAttributes && seg.flagAttributes.unpaved === true)) {
unpavedChip.click();
toggled = true;
}
}
}
if (!toggled) {
try {
const wzCheckbox = segPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked !== isUnpaved) {
hiddenInput.click();
toggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode unpaved toggle method failed: ' + e);
}
}
}
alertMessageParts.push(`Copied all attributes from connected segment.`);
log(`Copied all attributes from connected segment (fallback, no valid street name).`);
} else {
alertMessageParts.push(`No connected segment found to copy attributes.`);
}
}
} catch (error) {
console.error('Error copying all attributes:', error);
}
}, 100)
);
});
Promise.all(updatePromises).then(() => {
if (alertMessageParts.length) {
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.info('EZRoads Mod', alertMessageParts.join('
'), false, false, 5000);
} else {
alert('EZRoads Mod: ' + alertMessageParts.join('\n'));
}
}
// --- AUTOSAVE LOGIC HERE ---
if (options.autosave) {
setTimeout(() => {
log('Delayed Autosave starting...');
wmeSDK.Editing.save().then(() => {
log('Delayed Autosave completed.');
});
}, 600);
}
});
return;
}
selection.ids.forEach((origId, idx) => {
let id = origId;
let copyConnectedNameData = null;
// --- Pedestrian type switching logic ---
if (options.roadType) {
// If copySegmentName is enabled and switching Street → Pedestrian, prefetch connected segment name
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const currentIsPed = isPedestrianType(seg.roadType);
const targetIsPed = isPedestrianType(options.roadType);
if (!currentIsPed && targetIsPed && options.copySegmentName) {
// Find connected segment and store its name info
const fromNode = seg.fromNodeId;
const toNode = seg.toNodeId;
let connectedSegId = null;
const allSegs = wmeSDK.DataModel.Segments.getAll();
for (let s of allSegs) {
if (s.id !== id && (s.fromNodeId === fromNode || s.toNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === toNode)) {
connectedSegId = s.id;
break;
}
}
if (connectedSegId) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
copyConnectedNameData = {
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
};
}
}
const newId = recreateSegmentIfNeeded(id, options.roadType, copyConnectedNameData);
if (!newId) return; // If failed, skip further updates
if (newId !== id) {
id = newId; // Use the new segment ID for further updates
}
}
// Road Type
updatePromises.push(
delayedUpdate(() => {
if (options.roadType) {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const selectedRoad = roadTypes.find((rt) => rt.value === options.roadType);
//alertMessageParts.push(`Road Type: ${selectedRoad.name}`);
//updatedRoadType = true;
log(`Segment ID: ${id}, Current Road Type: ${seg.roadType}, Target Road Type: ${options.roadType}, Target Road Name : ${selectedRoad.name}`); // Log current and target road type
if (seg.roadType === options.roadType) {
log(`Segment ID: ${id} already has the target road type: ${options.roadType}. Skipping update.`);
alertMessageParts.push(`Road Type: ${selectedRoad.name} exists. Skipping update.`);
updatedRoadType = true;
} else {
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
roadType: options.roadType,
});
log('Road type updated successfully.');
alertMessageParts.push(`Road Type: ${selectedRoad.name}`);
updatedRoadType = true;
} catch (error) {
console.error('Error updating road type:', error);
}
}
}
}, 200)
); // 200ms delay before road type update
// Set lock if enabled
updatePromises.push(
delayedUpdate(() => {
if (options.setLock) {
const rank = wmeSDK.State.getUserInfo().rank;
const selectedRoad = roadTypes.find((rt) => rt.value === options.roadType);
if (selectedRoad) {
let lockSetting = options.locks.find((l) => l.id === selectedRoad.id);
if (lockSetting) {
let toLock = lockSetting.lock;
if (toLock === 'HRCS') {
toLock = getHighestSegLock(id);
} else {
toLock = parseInt(toLock, 10);
toLock = Math.max(toLock - 1, 0); // Adjust to 0-based rank, ensuring it does not go below 0
}
if (rank < toLock) toLock = rank;
log(toLock);
try {
const seg = wmeSDK.DataModel.Segments.getById({
segmentId: id,
});
let displayLockLevel = toLock === 'HRCS' || isNaN(toLock) ? 'HRCS' : `L${toLock + 1}`;
let currentDisplayLockLevel;
if (seg.lockRank === 'HRCS') {
// Should not happen, but for safety
currentDisplayLockLevel = 'HRCS';
} else {
currentDisplayLockLevel = `L${seg.lockRank + 1}`;
}
if (seg.lockRank === toLock || (lockSetting.lock === 'HRCS' && currentDisplayLockLevel === displayLockLevel)) {
// Compare lock levels
log(`Segment ID: ${id} already has the target lock level: ${displayLockLevel}. Skipping update.`);
alertMessageParts.push(`Lock Level: ${displayLockLevel} exists. Skipping update.`);
updatedLockLevel = true;
} else {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
lockRank: toLock,
});
alertMessageParts.push(`Lock Level: ${displayLockLevel}`);
updatedLockLevel = true;
}
} catch (error) {
console.error('Error updating segment lock rank:', error);
}
}
}
}
}, 300)
); // 250ms delay before lock rank update
// Speed Limit - use road-specific speed if updateSpeed is enabled
updatePromises.push(
delayedUpdate(() => {
if (options.updateSpeed) {
const selectedRoad = roadTypes.find((rt) => rt.value === options.roadType);
if (selectedRoad) {
const speedSetting = options.speeds.find((s) => s.id === selectedRoad.id);
log('Selected road for speed: ' + selectedRoad.name);
log('Speed setting found: ' + (speedSetting ? 'yes' : 'no'));
if (speedSetting) {
const speedValue = parseInt(speedSetting.speed, 10);
log('Speed value to set: ' + speedValue);
// If speedValue is 0 or less, treat as unset (undefined)
const speedToSet = !isNaN(speedValue) && speedValue > 0 ? speedValue : undefined;
const seg = wmeSDK.DataModel.Segments.getById({
segmentId: id,
});
if (seg.fwdSpeedLimit !== speedToSet || seg.revSpeedLimit !== speedToSet) {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: speedToSet,
revSpeedLimit: speedToSet,
});
alertMessageParts.push(`Speed Limit: ${speedToSet !== undefined ? speedToSet : 'unset'}`);
updatedSpeedLimit = true;
} else {
log(`Segment ID: ${id} already has the target speed limit: ${speedToSet}. Skipping update.`);
alertMessageParts.push(`Speed Limit: ${speedToSet !== undefined ? speedToSet : 'unset'} exists. Skipping update.`);
updatedSpeedLimit = true;
}
}
}
} else {
log('Speed updates disabled');
}
}, 400)
); // 300ms delay before lock rank update
// Handling the street
if (options.setStreet || options.setStreetCity || (!options.setStreet && !options.setStreetCity)) {
let city = null;
let street = null;
const segment = wmeSDK.DataModel.Segments.getById({ segmentId: id });
// --- City assignment logic ---
if (options.setStreetCity) {
// Checked: set city as none (empty city)
city = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
} else {
// Unchecked: try top city, then connected segment's city, then fallback to none
city = null;
// 1. Try top city
city = getTopCity();
// 2. If not found, try connected segment's city
if (!city) {
const connectedAddress = getFirstConnectedSegmentAddress(id);
if (connectedAddress && connectedAddress.city && connectedAddress.city.id) {
city = wmeSDK.DataModel.Cities.getById({ cityId: connectedAddress.city.id });
}
}
// 3. If still not found, fallback to none
if (!city) {
city = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
}
}
// --- Street assignment logic ---
if (options.setStreet) {
// Set street name to none and remove all alt street names
street = wmeSDK.DataModel.Streets.getStreet({
cityId: city.id,
streetName: '',
});
if (!street) {
street = wmeSDK.DataModel.Streets.addStreet({
streetName: '',
cityId: city.id,
});
}
// Remove all alternate street names
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: [],
});
} else if (options.setStreetCity) {
// Use the same street name as current, but in the empty city for both primary and all alts
const currentStreet =
segment && segment.primaryStreetId
? wmeSDK.DataModel.Streets.getById({
streetId: segment.primaryStreetId,
})
: null;
const streetName = currentStreet ? currentStreet.name || '' : '';
street = wmeSDK.DataModel.Streets.getStreet({
cityId: city.id,
streetName: streetName,
});
if (!street) {
street = wmeSDK.DataModel.Streets.addStreet({
streetName: streetName,
cityId: city.id,
});
}
// For all alternate street names, set them to the empty city as well
let newAltStreetIds = [];
if (segment && segment.alternateStreetIds) {
segment.alternateStreetIds.forEach((altStreetId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altStreetId });
if (altStreet && altStreet.name !== undefined) {
let altInCity = wmeSDK.DataModel.Streets.getStreet({
cityId: city.id,
streetName: altStreet.name || '',
});
if (!altInCity) {
altInCity = wmeSDK.DataModel.Streets.addStreet({
streetName: altStreet.name || '',
cityId: city.id,
});
}
newAltStreetIds.push(altInCity.id);
}
});
}
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: newAltStreetIds,
});
pushCityNameAlert(city.id, alertMessageParts);
updatedCityName = true;
} else {
// If both setStreet and setStreetCity are unchecked, always update city for primary and alt names
if (segment && (segment.primaryStreetId || (segment.alternateStreetIds && segment.alternateStreetIds.length))) {
// Update primary street to new city
let currentStreet = segment.primaryStreetId ? wmeSDK.DataModel.Streets.getById({ streetId: segment.primaryStreetId }) : null;
let streetName = currentStreet ? currentStreet.name || '' : '';
street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName });
if (!street) {
street = wmeSDK.DataModel.Streets.addStreet({ streetName, cityId: city.id });
}
// Update alt streets to new city
let newAltStreetIds = [];
if (segment && segment.alternateStreetIds && city) {
segment.alternateStreetIds.forEach((altStreetId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altStreetId });
if (altStreet && altStreet.name !== undefined) {
let altInCity = wmeSDK.DataModel.Streets.getStreet({
cityId: city.id,
streetName: altStreet.name || '',
});
if (!altInCity) {
altInCity = wmeSDK.DataModel.Streets.addStreet({
streetName: altStreet.name || '',
cityId: city.id,
});
}
newAltStreetIds.push(altInCity.id);
}
});
}
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: newAltStreetIds.length > 0 ? newAltStreetIds : undefined,
});
} else {
// New/empty street fallback
let autoCity = getTopCity() || getEmptyCity();
let autoStreet = wmeSDK.DataModel.Streets.getStreet({ cityId: autoCity.id, streetName: '' });
if (!autoStreet) {
autoStreet = wmeSDK.DataModel.Streets.addStreet({ streetName: '', cityId: autoCity.id });
}
street = autoStreet;
city = autoCity;
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: undefined,
});
}
}
log(`City Name: ${city?.name}, City ID: ${city?.id}, Street ID: ${street?.id}`);
}
// Updated unpaved handler with SegmentFlagAttributes and fallback
updatePromises.push(
delayedUpdate(() => {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const isPedestrian = isPedestrianType(seg.roadType);
if (isPedestrian) {
// Always set as paved for pedestrian types, regardless of checkbox
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let pavedToggled = false;
if (isUnpaved) {
// Click to set as paved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to paved for pedestrian type)');
pavedToggled = true;
}
}
if (!pavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to paved, non-compact mode, pedestrian type)');
pavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode paved toggle method failed: ' + e);
}
}
if (pavedToggled) {
alertMessageParts.push(`Paved Status: Paved (pedestrian type)`);
updatedPaved = true;
}
} else {
alertMessageParts.push(`Paved Status: Paved (pedestrian type, already set)`);
updatedPaved = true;
}
} else if (options.unpaved) {
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let unpavedToggled = false;
if (!isUnpaved) {
// Only click if segment is not already unpaved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to unpaved)');
unpavedToggled = true;
}
}
// If new method failed, try the old method as fallback for non-compact mode
if (!unpavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && !hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to unpaved, non-compact mode)');
unpavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode unpaved toggle method failed: ' + e);
}
}
if (unpavedToggled) {
alertMessageParts.push(`Paved Status: Unpaved`);
updatedPaved = true;
}
} else {
// Already unpaved, no action needed
alertMessageParts.push(`Paved Status: Unpaved (already set)`);
updatedPaved = true;
}
} else {
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let pavedToggled = false;
if (isUnpaved) {
// Click to set as paved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to paved)');
pavedToggled = true;
}
}
// If new method failed, try the old method as fallback for non-compact mode
if (!pavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to paved, non-compact mode)');
pavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode paved toggle method failed: ' + e);
}
}
if (pavedToggled) {
alertMessageParts.push(`Paved Status: Paved`);
updatedPaved = true;
}
} else {
// Already paved, no action needed
alertMessageParts.push(`Paved Status: Paved (already set)`);
updatedPaved = true;
}
}
}, 500)
); // 500ms delay for unpaved/paved toggle
// 3a. Copy segment name from connected segment if enabled
updatePromises.push(
delayedUpdate(() => {
if (options.copySegmentName) {
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const fromNode = seg.fromNodeId;
const connectedSegIds = getConnectedSegmentIDs(id);
// Gather all segments connected to fromNode (excluding self)
const fromNodeSegs = connectedSegIds.map((sid) => wmeSDK.DataModel.Segments.getById({ segmentId: sid })).filter((s) => s && (s.fromNodeId === fromNode || s.toNodeId === fromNode) && s.id !== id);
// Prefer the first fromNode segment with a name/city/alias
let preferredSeg = fromNodeSegs.find((s) => {
if (!s) return false;
const street = wmeSDK.DataModel.Streets.getById({ streetId: s.primaryStreetId });
const altStreetIds = s.alternateStreetIds || [];
let altNames = [];
altStreetIds.forEach((altId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) altNames.push(altStreet.name);
});
return street && (street.name || street.englishName || street.signText || altNames.length > 0);
});
let segsToTry = [];
if (preferredSeg) {
segsToTry.push(preferredSeg.id);
// Add the rest, excluding preferredSeg.id
segsToTry = segsToTry.concat(connectedSegIds.filter((cid) => cid !== preferredSeg.id));
} else {
segsToTry = connectedSegIds;
}
let found = false;
for (let connectedSegId of segsToTry) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
if (!connectedSeg) continue;
const streetId = connectedSeg.primaryStreetId;
const altStreetIds = connectedSeg.alternateStreetIds || [];
let street = wmeSDK.DataModel.Streets.getById({ streetId });
// Get alternate street names
let altNames = [];
altStreetIds.forEach((altId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) altNames.push(altStreet.name);
});
// If any connected segment has a name or alias, use it
if (street && (street.name || street.englishName || street.signText || altNames.length > 0)) {
if (options.setStreetCity && street) {
const emptyCity = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
// Try to find or create a street with the same name but in the empty city
let noneStreet = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: street.name || '',
});
if (!noneStreet) {
noneStreet = wmeSDK.DataModel.Streets.addStreet({
streetName: street.name || '',
cityId: emptyCity.id,
});
}
// For alternate streets, also convert them to the empty city
let newAltStreetIds = [];
altStreetIds.forEach((altId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) {
let altInEmptyCity = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: altStreet.name || '',
});
if (!altInEmptyCity) {
altInEmptyCity = wmeSDK.DataModel.Streets.addStreet({
streetName: altStreet.name || '',
cityId: emptyCity.id,
});
}
newAltStreetIds.push(altInEmptyCity.id);
}
});
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: noneStreet.id,
alternateStreetIds: newAltStreetIds,
});
let aliasMsg = altNames.length ? ` (Alternatives: ${altNames.join(', ')})` : '';
alertMessageParts.push(`Copied Name: ${street.name || ''}${aliasMsg}`);
updatedSegmentName = true;
pushCityNameAlert(emptyCity.id, alertMessageParts);
updatedCityName = true;
} else {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: streetId,
alternateStreetIds: altStreetIds,
});
let aliasMsg = altNames.length ? ` (Alternatives: ${altNames.join(', ')})` : '';
alertMessageParts.push(`Copied Name: ${street.name || ''}${aliasMsg}`);
updatedSegmentName = true;
pushCityNameAlert(street.cityId, alertMessageParts);
updatedCityName = true;
}
found = true;
break;
}
}
if (!found) {
alertMessageParts.push(`Copied Name: None (no connected segment found)`);
updatedSegmentName = true;
}
} catch (error) {
console.error('Error copying segment name:', error);
}
}
}, 100)
); // Run early in the update chain
});
Promise.all(updatePromises).then(() => {
// Always push city name alert if not already set by other actions
selection.ids.forEach((id) => {
if (!alertMessageParts.some((part) => part.startsWith('City Name'))) {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
if (seg && seg.primaryStreetId) {
const street = wmeSDK.DataModel.Streets.getById({
streetId: seg.primaryStreetId,
});
if (street) {
pushCityNameAlert(street.cityId, alertMessageParts);
updatedCityName = true;
}
}
}
});
const showAlert = () => {
const updatedFeatures = [];
if (updatedCityName) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('City')));
if (updatedSegmentName) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Copied Name')));
if (updatedRoadType) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Road Type')));
if (updatedLockLevel) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Lock Level')));
if (updatedSpeedLimit) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Speed Limit')));
if (updatedPaved) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Paved')));
const message = updatedFeatures.filter(Boolean).join(', ');
if (message) {
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.info('EZRoads Mod', `Segment updated with: ${message}`, false, false, 5000);
} else {
alert('EZRoads Mod: Segment updated (WazeWrap Alerts not available)');
}
}
};
// Autosave - DELAYED AUTOSAVE
if (options.autosave) {
setTimeout(() => {
log('Delayed Autosave starting...');
wmeSDK.Editing.save().then(() => {
log('Delayed Autosave completed.');
showAlert();
});
}, 600); // 1000ms (1 second) delay before autosave
} else {
showAlert();
}
});
};
const constructSettings = () => {
const localOptions = getOptions();
let currentRoadType = localOptions.roadType;
const update = (key, value) => {
const options = getOptions();
options[key] = value;
localOptions[key] = value;
saveOptions(options);
};
// Update lock level for a specific road type
const updateLockLevel = (roadTypeId, lockLevel) => {
const options = getOptions();
const lockIndex = options.locks.findIndex((l) => l.id === roadTypeId);
if (lockIndex !== -1) {
options.locks[lockIndex].lock = lockLevel; // Keep as string to handle 'HRCS'
localOptions.locks = options.locks;
saveOptions(options);
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.success('EZRoads Mod', 'Lock Levels saved!', false, false, 1500);
} else {
alert('EZRoads Mod: Lock Levels saved!');
}
}
};
// Update speed for a specific road type
const updateSpeed = (roadTypeId, speed) => {
const options = getOptions();
const speedIndex = options.speeds.findIndex((s) => s.id === roadTypeId);
let speedValue = parseInt(speed, 10);
if (isNaN(speedValue)) {
speedValue = -1;
}
log(`Updating speed for road type ${roadTypeId} to ${speedValue}`);
if (speedIndex !== -1) {
options.speeds[speedIndex].speed = speedValue;
localOptions.speeds = options.speeds;
saveOptions(options);
if (WazeWrap?.Alerts) {
WazeWrap.Alerts.success('EZRoads Mod', 'Speed Values saved!', false, false, 1500);
} else {
alert('EZRoads Mod: Speed Values saved!');
}
}
};
// Reset all options to defaults
const resetOptions = () => {
saveOptions(defaultOptions);
// Refresh the page to reload settings
window.location.reload();
};
// Checkbox option definitions
const checkboxOptions = [
{
id: 'setStreet',
text: 'Set Street Name to None',
key: 'setStreet',
tooltip: 'Sets the street name to None for selected segments. If unchecked, leaves the street name unchanged.',
},
{
id: 'setStreetCity',
text: 'Set city as none (uncheck to add auto)',
key: 'setStreetCity',
tooltip: 'If checked, sets the city to None for selected segments for both primary and alternate streets.. If unchecked, adds the available city name automatically to both primary and alt streets.',
},
{
id: 'autosave',
text: 'Autosave on Action',
key: 'autosave',
tooltip: 'Automatically saves after updating segments.',
},
{
id: 'unpaved',
text: 'Set as Unpaved (Uncheck for Paved)',
key: 'unpaved',
tooltip: 'Sets the segment as unpaved. Uncheck to set as paved.',
},
{
id: 'setLock',
text: 'Set the lock level',
key: 'setLock',
tooltip: 'Sets the lock level for the selected road type. It also enables the lock level dropdown.',
},
{
id: 'updateSpeed',
text: 'Update speed limits',
key: 'updateSpeed',
tooltip: 'Updates the speed limit for the selected road type. it also enables the speed input field.',
},
{
id: 'copySegmentName',
text: 'Copy connected Segment Name',
key: 'copySegmentName',
tooltip: "Copies the name and city from a connected segment to the selected segment. If 'Set city as none' is enabled, the city will be set to none regardless of the copied value.",
},
{
id: 'copySegmentAttributes',
text: 'Copy Connected Segment Attribute',
key: 'copySegmentAttributes',
tooltip: 'Copies all major attributes (road type, lock level, speed limits, paved/unpaved status, primary and alternate street names, and city) from a connected segment. When enabled, it overrides all other options except Autosave. Use shortcut key or (Quick Update Segment) to apply.',
},
];
// Helper function to create radio buttons
const createRadioButton = (roadType) => {
const id = `road-${roadType.id}`;
const isChecked = localOptions.roadType === roadType.value;
const lockSetting = localOptions.locks.find((l) => l.id === roadType.id) || { id: roadType.id, lock: 1 };
const speedSetting = localOptions.speeds.find((s) => s.id === roadType.id) || { id: roadType.id, speed: 40 };
const div = $(`