// ==UserScript==
// @name WME EZRoad Mod
// @namespace https://greasyfork.org/users/1087400
// @version 2.6.2
// @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/560385/code/WazeToastr.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 = `Bug Fix:
- Fixed auto city detection when "Set city as none" is unchecked. The script now properly checks connected segments for valid cities instead of defaulting to "None" when the displayed city is empty or unavailable.`;
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 hasValidCity = (id) => {
const addr = wmeSDK.DataModel.Segments.getAddress({ segmentId: id });
// Check if address has a city and the city is not empty
if (addr && addr.city && addr.city.id) {
const city = wmeSDK.DataModel.Cities.getById({ cityId: addr.city.id });
return city && !city.isEmpty;
}
return false;
};
while (segmentIDsToSearch.length > 0) {
const startSegmentID = segmentIDsToSearch.pop();
const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
log(`Checking connected segments for segment ${startSegmentID}: ${connectedSegmentIDs.join(', ')}`);
const hasValidCitySegmentId = connectedSegmentIDs.find(hasValidCity);
if (hasValidCitySegmentId) {
const addr = wmeSDK.DataModel.Segments.getAddress({ segmentId: hasValidCitySegmentId });
log(`Found valid city in connected segment ${hasValidCitySegmentId}`);
return addr;
}
nonMatches.push(startSegmentID);
connectedSegmentIDs.forEach((segmentID) => {
if (!nonMatches.includes(segmentID) && !segmentIDsToSearch.includes(segmentID)) {
segmentIDsToSearch.push(segmentID);
}
});
}
log('No valid city found in any connected segments');
return null;
}
// --- Helper to get the direction value from a segment for copying ---
function getDirectionFromSegment(segment) {
if (!segment) return null;
if (segment.isTwoWay) return 'TWO_WAY';
if (segment.isAtoB) return 'A_TO_B';
if (segment.isBtoA) return 'B_TO_A';
return null;
}
// --- Helper to copy all flag attributes from one segment to another ---
function copyFlagAttributes(fromSegmentId, toSegmentId) {
const fromSeg = wmeSDK.DataModel.Segments.getById({ segmentId: fromSegmentId });
const toSeg = wmeSDK.DataModel.Segments.getById({ segmentId: toSegmentId });
if (!fromSeg || !toSeg || !fromSeg.flagAttributes) {
return;
}
const segPanel = openPanel;
if (!segPanel) {
log('Segment panel not available for flag attribute updates');
return;
}
// Flag attribute mappings: { flagName: { selectorType, selector, checkedValue } }
const flagMappings = {
unpaved: { selectorType: 'checkbox', name: 'unpaved' },
};
for (let flagName in flagMappings) {
const mapping = flagMappings[flagName];
const fromValue = fromSeg.flagAttributes[flagName] === true;
const toValue = toSeg.flagAttributes && toSeg.flagAttributes[flagName] === true;
// Only update if values differ
if (fromValue === toValue) {
continue;
}
try {
// Try to find and click the checkbox
let checkboxFound = false;
// Try method 1: wz-checkable-chip with icon
const iconClass = flagName === 'unpaved' ? '.w-icon-unpaved-fill' : `.w-icon-${flagName.toLowerCase()}-fill`;
const unpavedIcon = segPanel.querySelector(iconClass);
if (unpavedIcon) {
const chip = unpavedIcon.closest('wz-checkable-chip');
if (chip) {
chip.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via chip`);
continue;
}
}
// Try method 2: wz-checkbox with name attribute
const wzCheckbox = segPanel.querySelector(`wz-checkbox[name="${mapping.name}"]`);
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector(`input[type="checkbox"][name="${mapping.name}"]`);
if (hiddenInput && hiddenInput.checked !== fromValue) {
hiddenInput.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via wz-checkbox`);
continue;
}
}
// Try method 3: regular checkbox
const regularCheckbox = segPanel.querySelector(`input[type="checkbox"][name="${mapping.name}"]`);
if (regularCheckbox && regularCheckbox.checked !== fromValue) {
regularCheckbox.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via regular checkbox`);
continue;
}
if (!checkboxFound) {
log(`Could not find UI element for flag attribute ${flagName}`);
}
} catch (e) {
log(`Error updating flag attribute ${flagName}: ${e}`);
}
}
}
const saveOptions = (options) => {
window.localStorage.setItem('WME_EZRoads_Mod_Options', JSON.stringify(options));
// Note: We don't clear current preset here, we check for modifications instead
};
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));
// If we're saving the current preset, it's now in sync
const currentPreset = getCurrentPresetName();
if (currentPreset === presetName) {
setCurrentPresetName(presetName); // Refresh to confirm it's current
}
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);
setCurrentPresetName(presetName);
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));
// Clear current preset if we're deleting it
if (getCurrentPresetName() === presetName) {
setCurrentPresetName(null);
}
return true;
};
const getCustomPresets = () => {
const presets = JSON.parse(window.localStorage.getItem('WME_EZRoads_Mod_CustomPresets')) || {};
return presets;
};
const getCurrentPresetName = () => {
return window.localStorage.getItem('WME_EZRoads_Mod_CurrentPreset') || null;
};
const setCurrentPresetName = (presetName) => {
if (presetName) {
window.localStorage.setItem('WME_EZRoads_Mod_CurrentPreset', presetName);
} else {
window.localStorage.removeItem('WME_EZRoads_Mod_CurrentPreset');
}
};
const isCurrentPresetModified = () => {
const currentPresetName = getCurrentPresetName();
if (!currentPresetName) return false;
const presets = getCustomPresets();
const preset = presets[currentPresetName];
if (!preset) {
setCurrentPresetName(null);
return false;
}
const currentOptions = getOptions();
// Compare locks and speeds
const locksMatch = JSON.stringify(currentOptions.locks) === JSON.stringify(preset.locks);
const speedsMatch = JSON.stringify(currentOptions.speeds) === JSON.stringify(preset.speeds);
return !(locksMatch && speedsMatch);
};
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 (WazeToastr?.Alerts) {
WazeToastr.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 (WazeToastr?.Alerts) {
WazeToastr.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) {
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: connectedSeg.fwdSpeedLimit,
revSpeedLimit: connectedSeg.revSpeedLimit,
roadType: connectedSeg.roadType,
lockRank: connectedSeg.lockRank,
elevationLevel: connectedSeg.elevationLevel,
direction: getDirectionFromSegment(connectedSeg),
});
} catch (updateError) {
log('updateSegment error (will retry with individual properties): ' + updateError);
// Fallback: try updating properties individually
const propsToTry = [
{ name: 'fwdSpeedLimit', value: connectedSeg.fwdSpeedLimit },
{ name: 'revSpeedLimit', value: connectedSeg.revSpeedLimit },
{ name: 'roadType', value: connectedSeg.roadType },
{ name: 'lockRank', value: connectedSeg.lockRank },
{ name: 'elevationLevel', value: connectedSeg.elevationLevel },
{ name: 'direction', value: getDirectionFromSegment(connectedSeg) },
];
for (let prop of propsToTry) {
try {
if (prop.value !== undefined && prop.value !== null) {
const updateObj = { segmentId: id };
updateObj[prop.name] = prop.value;
wmeSDK.DataModel.Segments.updateSegment(updateObj);
}
} catch (e) {
log(`Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log('updateAddress error: ' + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
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 });
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: connectedSeg.fwdSpeedLimit,
revSpeedLimit: connectedSeg.revSpeedLimit,
roadType: connectedSeg.roadType,
lockRank: connectedSeg.lockRank,
elevationLevel: connectedSeg.elevationLevel,
direction: getDirectionFromSegment(connectedSeg),
});
} catch (updateError) {
log('updateSegment error in fallback (will retry with individual properties): ' + updateError);
// Fallback: try updating properties individually
const propsToTry = [
{ name: 'fwdSpeedLimit', value: connectedSeg.fwdSpeedLimit },
{ name: 'revSpeedLimit', value: connectedSeg.revSpeedLimit },
{ name: 'roadType', value: connectedSeg.roadType },
{ name: 'lockRank', value: connectedSeg.lockRank },
{ name: 'elevationLevel', value: connectedSeg.elevationLevel },
{ name: 'direction', value: getDirectionFromSegment(connectedSeg) },
];
for (let prop of propsToTry) {
try {
if (prop.value !== undefined && prop.value !== null) {
const updateObj = { segmentId: id };
updateObj[prop.name] = prop.value;
wmeSDK.DataModel.Segments.updateSegment(updateObj);
}
} catch (e) {
log(`Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log('updateAddress error in fallback: ' + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
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 (WazeToastr?.Alerts) {
WazeToastr.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 (null for removal)
// Use null instead of undefined to properly remove speed limits
const speedToSet = !isNaN(speedValue) && speedValue > 0 ? speedValue : null;
const seg = wmeSDK.DataModel.Segments.getById({
segmentId: id,
});
// Compare using loose equality (==) to treat null and undefined as equivalent
// This ensures we don't try to update when segment already has no speed limit
const needsUpdate = seg.fwdSpeedLimit != speedToSet || seg.revSpeedLimit != speedToSet;
log(`Current fwd speed: ${seg.fwdSpeedLimit}, rev speed: ${seg.revSpeedLimit}, target speed: ${speedToSet}, needs update: ${needsUpdate}`);
if (needsUpdate) {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: speedToSet,
revSpeedLimit: speedToSet,
});
alertMessageParts.push(`Speed Limit: ${speedToSet !== null ? speedToSet : 'unset'}`);
updatedSpeedLimit = true;
} else {
log(`Segment ID: ${id} already has the target speed limit: ${speedToSet}. Skipping update.`);
alertMessageParts.push(`Speed Limit: ${speedToSet !== null ? 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();
log(`Top city: ${city ? `name="${city.name}", isEmpty=${city.isEmpty}, id=${city.id}` : 'null'}`);
// 2. If not found or empty, try connected segment's city
if (!city || city.isEmpty) {
log('Top city not found or empty, checking connected segments...');
const connectedAddress = getFirstConnectedSegmentAddress(id);
if (connectedAddress && connectedAddress.city && connectedAddress.city.id) {
const connectedCity = wmeSDK.DataModel.Cities.getById({ cityId: connectedAddress.city.id });
log(`Connected segment city: ${connectedCity ? `name="${connectedCity.name}", isEmpty=${connectedCity.isEmpty}, id=${connectedCity.id}` : 'null'}`);
// Only use connected city if it's not empty
if (connectedCity && !connectedCity.isEmpty) {
city = connectedCity;
}
} else {
log('No connected address found');
}
}
// 3. If still not found or empty, fallback to none
if (!city || city.isEmpty) {
log('No valid city found, using empty 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 (WazeToastr?.Alerts) {
WazeToastr.Alerts.info('EZRoads Mod', `Segment updated with: ${message}`, false, false, 3000);
} else {
alert('EZRoads Mod: Segment updated (WazeToastr 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);
// Trigger preset list refresh if settings panel is open
if ($('#ezroadsmod-presets-list').length) {
$('#ezroadsmod-presets-list').trigger('refresh-presets');
}
if (WazeToastr?.Alerts) {
WazeToastr.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);
// Trigger preset list refresh if settings panel is open
if ($('#ezroadsmod-presets-list').length) {
$('#ezroadsmod-presets-list').trigger('refresh-presets');
}
if (WazeToastr?.Alerts) {
WazeToastr.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 = $(`