Easily update roads
// ==UserScript==
// @name WME EZRoad Mod
// @namespace https://greatest.deepsurf.us/users/1087400
// @version 2.6.8.7
// @description Easily update roads
// @author https://greatest.deepsurf.us/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 greatest.deepsurf.us
// @require https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
// @require https://greatest.deepsurf.us/scripts/560385/code/WazeToastr.js
// ==/UserScript==
/*Script modified from WME EZRoad (https://greatest.deepsurf.us/en/scripts/518381-wme-ezsegments) original author: Michaelrosstarr and thanks to him*/
(function main() {
('use strict');
const updateMessage = `<strong>Version 2.6.8.7 - 2026-03-01:</strong><br>
- Fixed found bug fixes.<br>
<br>`;
const scriptName = GM_info.script.name;
const scriptVersion = GM_info.script.version;
const downloadUrl = 'https://greatest.deepsurf.us/en/scripts/528552-wme-ezroad-mod/code/WME%20EZRoad%20Mod.user.js';
const forumURL = 'https://greatest.deepsurf.us/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,
showSegmentLength: false,
checkGeometryIssues: false,
geometryIssueThreshold: 2,
enableUTurn: false,
restrictExceptMotorbike: 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 UserRankRequiredForGeometryFix = 3; // Minimum user rank required to use the geometry fix feature - only show for L3 and above (rank >= 2 in SDK)
const log = (message) => {
if (typeof message === 'string') {
console.log(`$${scriptName}: ` + message);
} else {
console.log(`$${scriptName}: `, 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) => {
try {
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 });
// Ensure city object is fully loaded with name property
return city && !city.isEmpty && city.name !== undefined;
}
} catch (e) {
log(`Error checking city for segment ${id}: ${e}`);
}
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;
}
try {
// Use WME SDK updateSegment to copy unpaved flag attribute
const fromUnpavedValue = fromSeg.flagAttributes.unpaved === true;
const toUnpavedValue = toSeg.flagAttributes && toSeg.flagAttributes.unpaved === true;
// Only update if values differ
if (fromUnpavedValue !== toUnpavedValue) {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: toSegmentId,
flagAttributes: {
unpaved: fromUnpavedValue
}
});
log(`Copied flag attribute unpaved=${fromUnpavedValue} via SDK from segment ${fromSegmentId} to ${toSegmentId}`);
} else {
log(`Flag attribute unpaved already matches (${fromUnpavedValue}) between segments`);
}
} catch (e) {
log(`Error copying flag attributes via SDK: ${e}`);
}
}
// --- NEW: Helper to apply motorbike-only restrictions to a segment via UI automation ---
function applyMotorbikeOnlyRestriction(segmentId) {
/**
* Applies vehicle restrictions to allow only motorbikes on a segment.
* Uses DOM manipulation to automate the WME UI since the SDK doesn't support this yet.
*/
return new Promise((resolve) => {
try {
const segment = wmeSDK.DataModel.Segments.getById({ segmentId });
if (!segment || isPedestrianType(segment.roadType)) {
const roadTypeName = segment ? roadTypes.find(rt => rt.value === segment.roadType)?.name || 'Unknown' : 'N/A';
log(`Segment ${segmentId} not found or pedestrian type ${roadTypeName} (${segment?.roadType || 'N/A'}), cannot apply motorbike restriction`);
WazeToastr.Alerts.warning(`${scriptName}`, `Segment not found or "${roadTypeName}" is not supported type, cannot apply motorbike restriction`, false, false, 5000);
resolve('not_supported type');
return;
}
log(`Applying motorbike-only restriction to segment ${segmentId} via UI automation`);
/* ===== WME SDK APPROACH (NOT YET SUPPORTED - COMMENTED OUT FOR FUTURE USE) =====
// Get SDK constants - try different possible locations
const RESTRICTION_TYPE = wmeSDK.RESTRICTION_TYPE || wmeSDK.Constants?.RESTRICTION_TYPE || {
FREE: 'FREE',
BLOCKED: 'BLOCKED',
DIFFICULT: 'DIFFICULT',
TOLL: 'TOLL'
};
const VEHICLE_TYPE = wmeSDK.VEHICLE_TYPE || wmeSDK.Constants?.VEHICLE_TYPE || {
MOTORCYCLE: 'MOTORCYCLE',
CAR: 'CAR',
TAXI: 'TAXI',
BUSES: 'BUSES',
TRUCKS: 'TRUCKS',
SCOOTERS: 'SCOOTERS'
};
// Create motorcycle-only restriction using SDK constants and structure
// Only motorcycles are allowed (FREE restriction), all other vehicles are BLOCKED
const motorcycleOnlyRestriction = {
driveProfiles: {
// FREE: Only motorcycles can pass freely
[RESTRICTION_TYPE.FREE]: [
{
vehicleTypes: [VEHICLE_TYPE.MOTORCYCLE],
licensePlateNumber: '',
numPassengers: 0,
subscriptions: [],
},
],
// BLOCKED: All other vehicle types are blocked
[RESTRICTION_TYPE.BLOCKED]: [
{
vehicleTypes: [
VEHICLE_TYPE.CAR,
VEHICLE_TYPE.TAXI,
VEHICLE_TYPE.BUSES,
VEHICLE_TYPE.TRUCKS,
VEHICLE_TYPE.SCOOTERS,
],
licensePlateNumber: '',
numPassengers: 0,
subscriptions: [],
},
],
[RESTRICTION_TYPE.DIFFICULT]: [],
[RESTRICTION_TYPE.TOLL]: [],
},
isExpired: false,
};
// Try applying via SDK (currently not working)
// Method 1: Try Segments.addRestriction
if (wmeSDK.DataModel.Segments.addRestriction) {
wmeSDK.DataModel.Segments.addRestriction({
segmentId,
restriction: motorcycleOnlyRestriction,
});
}
// Method 2: Try Segments.addSegmentRestriction
if (wmeSDK.DataModel.Segments.addSegmentRestriction) {
wmeSDK.DataModel.Segments.addSegmentRestriction({
segmentId,
restriction: motorcycleOnlyRestriction,
});
}
// Method 3: Try updateSegment with restrictions array
const currentRestrictions = segment.restrictions || [];
wmeSDK.DataModel.Segments.updateSegment({
segmentId,
restrictions: [...currentRestrictions, motorcycleOnlyRestriction],
});
// Method 4: Try SegmentRestrictions API if it exists
if (wmeSDK.DataModel.SegmentRestrictions?.addRestriction) {
wmeSDK.DataModel.SegmentRestrictions.addRestriction({
segmentId,
restriction: motorcycleOnlyRestriction,
direction: 'BOTH',
});
}
===== END WME SDK APPROACH ===== */
// Helper function to wait for element
const waitForElement = (selector, timeout = 5000) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkInterval = setInterval(() => {
const element = document.querySelector(selector);
if (element) {
clearInterval(checkInterval);
resolve(element);
} else if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
reject(new Error(`Timeout waiting for element: ${selector}`));
}
}, 100);
});
};
// Helper to click element
const clickElement = (element) => {
if (element) {
element.click();
log(`Clicked: ${element.tagName} ${element.className}`);
return true;
}
return false;
};
// Step 1: Click "Add restrictions" button
setTimeout(() => {
const addRestrictionsBtn = document.querySelector('wz-button.edit-restrictions');
if (!addRestrictionsBtn) {
log('Add restrictions button not found');
resolve('not_supported');
return;
}
clickElement(addRestrictionsBtn);
// Step 2: Wait for modal and click "Add new" for bidirectional (2-way)
setTimeout(() => {
waitForElement('.bidi-restrictions-summary .do-create')
.then((addNewBtn) => {
clickElement(addNewBtn);
// Step 3: Wait for disposition dropdown and select "Entire Segment" (value="1")
setTimeout(() => {
waitForElement('select[name="disposition"]')
.then((dispositionSelect) => {
dispositionSelect.value = '1'; // Entire Segment
dispositionSelect.dispatchEvent(new Event('change', { bubbles: true }));
log('Selected: Entire Segment');
// Step 4: Click the plus icon to add restriction type
setTimeout(() => {
const plusIcon = document.querySelector('.fa-plus');
if (plusIcon && clickElement(plusIcon)) {
// Step 5: Wait for and click "Vehicle type" option
setTimeout(() => {
waitForElement('wz-menu-item')
.then(() => {
const menuItems = document.querySelectorAll('wz-menu-item');
let vehicleTypeItem = null;
menuItems.forEach(item => {
if (item.textContent.includes('Vehicle type')) {
vehicleTypeItem = item;
}
});
if (vehicleTypeItem && clickElement(vehicleTypeItem)) {
// Step 6: Wait for vehicle type dropdown and select Motorcycle
setTimeout(() => {
waitForElement('.do-set-vehicle-type')
.then(() => {
const vehicleOptions = document.querySelectorAll('.do-set-vehicle-type');
let motorcycleOption = null;
vehicleOptions.forEach(option => {
if (option.textContent.toLowerCase().includes('motorcycle')) {
motorcycleOption = option;
}
});
if (motorcycleOption && clickElement(motorcycleOption)) {
log('Selected: Motorcycle');
// Step 7: Click the Add button
setTimeout(() => {
waitForElement('button.do-create')
.then((addBtn) => {
if (clickElement(addBtn)) {
log('Clicked Add button');
// Click Apply button to save
setTimeout(() => {
const applyBtn = document.querySelector('button.do-apply');
if (applyBtn && clickElement(applyBtn)) {
log('Successfully applied motorbike-only restriction via UI automation');
resolve(true);
} else {
log('Apply button not found');
resolve('not_supported');
}
}, 100);
} else {
resolve('not_supported');
}
})
.catch(err => {
log(`Error finding Add button: ${err}`);
resolve('not_supported');
});
}, 100);
} else {
log('Motorcycle option not found');
resolve('not_supported');
}
})
.catch(err => {
log(`Error finding vehicle options: ${err}`);
resolve('not_supported');
});
}, 100);
} else {
log('Vehicle type menu item not found');
resolve('not_supported');
}
})
.catch(err => {
log(`Error finding menu items: ${err}`);
resolve('not_supported');
});
}, 100);
} else {
log('Plus icon not found');
resolve('not_supported');
}
}, 100);
})
.catch(err => {
log(`Error finding disposition dropdown: ${err}`);
resolve('not_supported');
});
}, 100);
})
.catch(err => {
log(`Error finding Add new button: ${err}`);
resolve('not_supported');
});
}, 100);
}, 50);
} catch (error) {
log(`Error in applyMotorbikeOnlyRestriction: ${error}`);
resolve(false);
}
});
}
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);
};
// ***--- Legacy Keyboard Shortcuts System (from WME Street to River PLUS) ---***
function WMEKSRegisterKeyboardShortcut(scriptName, shortcutsHeader, newShortcut, shortcutDescription, functionToCall, shortcutKeysObj, arg) {
try {
I18n.translations[I18n.locale].keyboard_shortcuts.groups[scriptName].members.length;
} catch (c) {
(W.accelerators.Groups[scriptName] = []),
(W.accelerators.Groups[scriptName].members = []),
(I18n.translations[I18n.locale].keyboard_shortcuts.groups[scriptName] = []),
(I18n.translations[I18n.locale].keyboard_shortcuts.groups[scriptName].description = shortcutsHeader),
(I18n.translations[I18n.locale].keyboard_shortcuts.groups[scriptName].members = []);
}
if (functionToCall && 'function' == typeof functionToCall) {
(I18n.translations[I18n.locale].keyboard_shortcuts.groups[scriptName].members[newShortcut] = shortcutDescription),
W.accelerators.addAction(newShortcut, {
group: scriptName,
});
var i = '-1',
j = {};
(j[i] = newShortcut),
W.accelerators._registerShortcuts(j),
null !== shortcutKeysObj && ((j = {}), (j[shortcutKeysObj] = newShortcut), W.accelerators._registerShortcuts(j)),
W.accelerators.events.register(newShortcut, null, function () {
functionToCall(arg);
});
} else alert('The function ' + functionToCall + ' has not been declared');
}
function WMEKSLoadKeyboardShortcuts(scriptName) {
console.log(`${scriptName} Loading keyboard shortcuts for ${scriptName}`);
if (localStorage[scriptName + 'KBS']) {
const shortcuts = JSON.parse(localStorage[scriptName + 'KBS']);
for (let i = 0; i < shortcuts.length; i++) {
try {
W.accelerators._registerShortcuts(shortcuts[i]);
} catch (error) {
console.error(`${scriptName} Error registering shortcut:`, error);
}
}
}
}
function WMEKSSaveKeyboardShortcuts(scriptName) {
console.log(`${scriptName} Saving keyboard shortcuts for ${scriptName}`);
try {
WazeToastr.Alerts.success(`${scriptName}`, `Saving keyboard shortcuts for ${scriptName}`, false, false, 3000);
} catch (e) {
console.warn(`${scriptName} WazeToastr.Alerts.success failed:`, e);
}
const shortcuts = [];
for (var actionName in W.accelerators.Actions) {
var shortcutString = '';
if (W.accelerators.Actions[actionName].group == scriptName) {
W.accelerators.Actions[actionName].shortcut
? (W.accelerators.Actions[actionName].shortcut.altKey === !0 && (shortcutString += 'A'),
W.accelerators.Actions[actionName].shortcut.shiftKey === !0 && (shortcutString += 'S'),
W.accelerators.Actions[actionName].shortcut.ctrlKey === !0 && (shortcutString += 'C'),
'' !== shortcutString && (shortcutString += '+'),
W.accelerators.Actions[actionName].shortcut.keyCode && (shortcutString += W.accelerators.Actions[actionName].shortcut.keyCode))
: (shortcutString = '-1');
var shortcutObj = {};
(shortcutObj[shortcutString] = W.accelerators.Actions[actionName].id), (shortcuts[shortcuts.length] = shortcutObj);
}
}
localStorage[scriptName + 'KBS'] = JSON.stringify(shortcuts);
}
// Helper function to handle toggle logic
function handleToggle(optionKey, featureName) {
const options = getOptions();
options[optionKey] = !options[optionKey];
saveOptions(options);
// Update checkbox in DOM
const checkboxId = optionKey;
const $checkbox = $(`#${checkboxId}`);
if ($checkbox.length > 0) {
$checkbox.prop('checked', options[optionKey]);
// Trigger the same logic that would be triggered by manual checkbox click
// Mutually exclusive logic for setStreet and copySegmentName
if (optionKey === 'setStreet' && options[optionKey]) {
$('#copySegmentName').prop('checked', false);
saveOptions({ ...getOptions(), copySegmentName: false });
}
if (optionKey === 'copySegmentName' && options[optionKey]) {
$('#setStreet').prop('checked', false);
saveOptions({ ...getOptions(), setStreet: false });
}
// Mutual exclusion logic for copySegmentAttributes
if (optionKey === 'copySegmentAttributes') {
if (options[optionKey]) {
// Uncheck all other checkboxes except autosave, showSegmentLength, checkGeometryIssues, restrictExceptMotorbike
$('.ezroadsmod-other-checkbox').each(function () {
$(this).prop('checked', false);
});
const newOpts = getOptions();
newOpts.setStreet = false;
newOpts.setStreetCity = false;
newOpts.unpaved = false;
newOpts.setLock = false;
newOpts.updateSpeed = false;
newOpts.enableUTurn = false;
newOpts.copySegmentName = false;
newOpts.copySegmentAttributes = true;
saveOptions(newOpts);
}
} else if (optionKey !== 'autosave' && optionKey !== 'showSegmentLength' && optionKey !== 'checkGeometryIssues' && optionKey !== 'restrictExceptMotorbike') {
// If any other checkbox (except autosave, showSegmentLength, checkGeometryIssues, restrictExceptMotorbike) is checked, uncheck copySegmentAttributes
if (options[optionKey]) {
$('#copySegmentAttributes').prop('checked', false);
const newOpts = getOptions();
newOpts.copySegmentAttributes = false;
saveOptions(newOpts);
}
}
// Handle Segment Length / Geometry Check toggle
if (optionKey === 'showSegmentLength' || optionKey === 'checkGeometryIssues' || optionKey === 'copySegmentAttributes') {
if (typeof handleSegmentLengthToggle === 'function') {
handleSegmentLengthToggle();
}
}
}
// Show notification
if (WazeToastr?.Alerts) {
const status = options[optionKey] ? 'Enabled' : 'Disabled';
WazeToastr.Alerts.info(`${scriptName}`, `${featureName}: ${status}`, false, false, 2000);
}
console.log(`[${scriptName}] [${featureName}] Toggled to: ${options[optionKey]}`);
}
// Initialize all action shortcuts using legacy mode (following WME POI Shortcuts pattern)
const initializeActionShortcuts = () => {
try {
// Legacy shortcuts configuration - following POI Shortcuts pattern
var shortcutsConfig = [
{
handler: 'WME_EZRoad_Mod_SetStreetNameToNone',
title: 'Set Street Name to None',
func: function (arg) {
handleToggle('setStreet', 'Set Street Name to None');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_SetCityAsNone',
title: 'Set City as None',
func: function (arg) {
handleToggle('setStreetCity', 'Set City as None');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_AutosaveOnAction',
title: 'Autosave on Action',
func: function (arg) {
handleToggle('autosave', 'Autosave on Action');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_SetAsUnpaved',
title: 'Set as Unpaved',
func: function (arg) {
handleToggle('unpaved', 'Set as Unpaved');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_SetLockLevel',
title: 'Set Lock Level',
func: function (arg) {
handleToggle('setLock', 'Set Lock Level');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_UpdateSpeedLimits',
title: 'Update Speed Limits',
func: function (arg) {
handleToggle('updateSpeed', 'Update Speed Limits');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_EnableUTurn',
title: 'Enable U-Turn',
func: function (arg) {
handleToggle('enableUTurn', 'Enable U-Turn');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_CopyConnectedSegmentName',
title: 'Copy Connected Segment Name',
func: function (arg) {
handleToggle('copySegmentName', 'Copy Connected Segment Name');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_CopyConnectedSegmentAttribute',
title: 'Copy Connected Segment Attribute',
func: function (arg) {
handleToggle('copySegmentAttributes', 'Copy Connected Segment Attribute');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_ShowSegmentLength',
title: 'Show Segment Length <=20m',
func: function (arg) {
handleToggle('showSegmentLength', 'Show Segment Length <=20m');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_CheckGeometryIssues',
title: 'Check Geometry Issues',
func: function (arg) {
handleToggle('checkGeometryIssues', 'Check Geometry Issues');
},
key: -1,
arg: {},
},
{
handler: 'WME_EZRoad_Mod_RestrictMotorbikesOnly',
title: 'Restrict Except Motorbike',
func: function (arg) {
handleToggle('restrictExceptMotorbike', 'Restrict Except Motorbike');
},
key: -1,
arg: {},
},
];
// Register legacy shortcuts
for (var i = 0; i < shortcutsConfig.length; ++i) {
WMEKSRegisterKeyboardShortcut(scriptName, 'EZRoad Mod - Feature Toggles', shortcutsConfig[i].handler, shortcutsConfig[i].title, shortcutsConfig[i].func, shortcutsConfig[i].key, shortcutsConfig[i].arg);
}
// Load any previously saved shortcuts
WMEKSLoadKeyboardShortcuts(scriptName);
// Save shortcuts before page unload
window.addEventListener(
'beforeunload',
function () {
WMEKSSaveKeyboardShortcuts(scriptName);
},
false
);
console.log(`${scriptName} All action shortcuts initialized successfully with legacy mode`);
} catch (e) {
console.error(`${scriptName} Error initializing action shortcuts:`, e);
}
};
// ***--- End of Legacy Keyboard Shortcuts System but below has initialization ---***
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');
}
// Initialize all action shortcuts for the 12 features using the legacy shortcuts system (following WME POI Shortcuts pattern)
// WazeToastr is guaranteed to be available at this point (checked during initScript)
try {
initializeActionShortcuts();
console.log(`${scriptName} Action shortcuts initialized`);
} catch (e) {
console.error(`${scriptName} Error initializing action shortcuts:`, e);
}
// ***--- End of Legacy Keyboard Shortcuts System initialization ---***
// --- 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(`${scriptName} 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(`${scriptName} 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(`${scriptName} 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(`${scriptName}`, `Selected road type: <b>${rt.name}</b>`, false, false, 1500);
}
},
description: `Select road type: ${rt.name}`,
shortcutId,
shortcutKeys: rt.shortcutKey,
});
} catch (e) {
log(`Shortcut registration failed for ${rt.name}: ${e}`);
}
}
});
// Register shortcut for Motorcycle Only restriction
const motorcycleShortcutId = `EZRoad_Mod_MotorcycleOnlyRestriction`;
// Prevent duplicate shortcut registration
if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId: motorcycleShortcutId })) {
try {
wmeSDK.Shortcuts.createShortcut({
callback: () => {
const selection = wmeSDK.Editing.getSelection();
if (!selection || selection.objectType !== 'segment' || !selection.ids || selection.ids.length === 0) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.warning(`${scriptName}`, 'Please select one or more segments first', false, false, 3000);
}
return;
}
// Apply the restriction via UI automation
applyMotorbikeOnlyRestriction(selection.ids[0]).then((result) => {
if (result === true) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(
`${scriptName}`,
`Motorbike-only restriction applied to ${selection.ids.length} segment(s) ✓`,
false,
false,
3000
);
}
} else if (result === 'not_supported') {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.warning(`${scriptName}`, `Segment not found or is pedestrian type, cannot apply motorbike restriction`, false, false, 5000);
}
} else if (result === 'not_supported type') {
log(`${scriptName} Segment not supported type, cannot apply motorbike restriction`);
}
}).catch((error) => {
console.error(`${scriptName} Error applying motorbike restriction:`, error);
});
},
description: `Apply Motorbike-Only Restriction to Selected Segments`,
shortcutId: motorcycleShortcutId,
shortcutKeys: 'A+R',
});
} catch (e) {
log(`Shortcut registration failed for ${motorcycleShortcutId}: ${e}`);
}
}
// Initialize segment length display layer
initSegmentLengthLayer();
// Inject Geometry Fix Button
setInterval(addGeometryFixButton, 2000);
log('Completed Init');
};
// ===== Geometry Quality Check Helper =====
/**
* Checks if any intermediate geometry nodes are too close to segment endpoints
* @param {Object} segment - WME segment object
* @param {number} thresholdMeters - Distance threshold in meters (default: 2)
* @returns {Object} { hasIssue: boolean, details: Array }
*/
function checkGeometryNodePlacement(segment, thresholdMeters = 2) {
if (!segment || !segment.geometry || !segment.geometry.coordinates) {
return { hasIssue: false, details: [] };
}
if (typeof turf === 'undefined') {
log('ERROR: Turf.js is not loaded!');
return { hasIssue: false, details: [] };
}
const coords = segment.geometry.coordinates;
// Need at least 3 points (start, intermediate, end) to have geometry nodes
if (coords.length < 3) {
return { hasIssue: false, details: [] };
}
const nodeA = turf.point(coords[0]); // First coordinate (Node A)
const nodeB = turf.point(coords[coords.length - 1]); // Last coordinate (Node B)
const issues = [];
// Check intermediate points (geometry nodes)
for (let i = 1; i < coords.length - 1; i++) {
const geometryNode = turf.point(coords[i]);
// Calculate distance to Node A
const distanceToA = turf.distance(geometryNode, nodeA, { units: 'meters' });
if (distanceToA <= thresholdMeters) {
issues.push({
nodeIndex: i,
distanceToA: distanceToA,
distanceToB: null,
closeTo: 'A',
coordinates: coords[i],
});
}
// Calculate distance to Node B
const distanceToB = turf.distance(geometryNode, nodeB, { units: 'meters' });
if (distanceToB <= thresholdMeters) {
issues.push({
nodeIndex: i,
distanceToA: null,
distanceToB: distanceToB,
closeTo: 'B',
coordinates: coords[i],
});
}
}
return {
hasIssue: issues.length > 0,
details: issues,
segmentId: segment.id,
totalGeometryNodes: coords.length - 2, // Exclude start and end
};
}
// ===== Segment Length Display Functionality =====
let segmentLengthContainer = null;
let segmentLabelCache = []; // Cache segment data and label elements
// Store last map bounds to detect changes
let lastBounds = null;
let lastZoom = null;
let updateInterval = null;
let isMapMoving = false;
let updateFrameRequest = null;
// Define helper functions first
function clearSegmentLengthDisplay() {
if (segmentLengthContainer) {
segmentLengthContainer.innerHTML = '';
}
segmentLabelCache = [];
}
// Rebuild segment data and create new labels (expensive - only on zoom/data changes)
function rebuildSegmentLengthDisplay() {
const options = getOptions();
// Update dashboard count if exists (even if hidden)
const countBadge = document.getElementById('ezroad-geometry-error-count');
if ((!options.showSegmentLength && !options.checkGeometryIssues) || !segmentLengthContainer) {
if (countBadge) countBadge.style.display = 'none';
return;
}
clearSegmentLengthDisplay();
if (typeof turf === 'undefined') {
log('ERROR: Turf.js is not loaded!');
return;
}
let issueCount = 0; // Count for geometry nodes near endpoints (📍 pin icon - bug button)
const segmentsWithIssues = new Set(); // Track unique segments with geometry node issues
try {
const currentZoom = wmeSDK.Map.getZoomLevel();
if (currentZoom < 18) {
if (countBadge) countBadge.style.display = 'none';
return;
}
const allSegments = wmeSDK.DataModel.Segments.getAll();
let extent = wmeSDK.Map.getMapExtent();
if (!extent || !allSegments || allSegments.length === 0) {
if (countBadge) countBadge.style.display = 'none';
return;
}
const mapBounds = {
west: extent[0],
south: extent[1],
east: extent[2],
north: extent[3],
};
// Use a DocumentFragment to batch DOM insertions (Performance optimization)
const fragment = document.createDocumentFragment();
allSegments.forEach((segment) => {
try {
const geometry = segment.geometry;
if (!geometry || !geometry.coordinates || geometry.coordinates.length < 2) {
return;
}
// Skip roundabouts (segments that are part of a junction)
if (segment.junctionId !== null) {
return;
}
// 1. Check for geometry nodes near endpoints
if (options.checkGeometryIssues) {
const geoResult = checkGeometryNodePlacement(segment, options.geometryIssueThreshold);
if (geoResult.hasIssue) {
let hasVisibleIssue = false;
geoResult.details.forEach((issue) => {
// Check visibility
if (issue.coordinates[0] < mapBounds.west || issue.coordinates[0] > mapBounds.east || issue.coordinates[1] < mapBounds.south || issue.coordinates[1] > mapBounds.north) {
return;
}
hasVisibleIssue = true;
const pinDiv = document.createElement('div');
pinDiv.innerHTML = '📍'; // Pin icon
pinDiv.style.position = 'absolute';
pinDiv.style.width = '30px';
pinDiv.style.height = '30px';
pinDiv.style.display = 'flex';
pinDiv.style.alignItems = 'center';
pinDiv.style.justifyContent = 'center';
pinDiv.style.fontSize = '30px';
pinDiv.style.pointerEvents = 'none';
pinDiv.title = `Node too close to ${issue.closeTo === 'A' ? 'start' : 'end'}: ${Math.round(issue.distanceToA || issue.distanceToB * 10) / 10}m`;
fragment.appendChild(pinDiv);
segmentLabelCache.push({
lon: issue.coordinates[0],
lat: issue.coordinates[1],
labelDiv: pinDiv,
offsetX: 15, // Center of 30px
offsetY: 30, // Shift up (full height)
});
});
// Count unique segments with visible issues
if (hasVisibleIssue) {
segmentsWithIssues.add(segment.id);
}
}
}
// 2. Show Segment Length
if (options.showSegmentLength) {
const line = turf.lineString(geometry.coordinates);
const lengthMeters = turf.length(line, { units: 'meters' });
if (lengthMeters <= 20) {
const midPointFeature = turf.along(line, lengthMeters / 2, { units: 'meters' });
const midCoords = midPointFeature.geometry.coordinates;
if (midCoords[0] >= mapBounds.west && midCoords[0] <= mapBounds.east && midCoords[1] >= mapBounds.south && midCoords[1] <= mapBounds.north) {
// Create label element
const labelDiv = document.createElement('div');
labelDiv.style.position = 'absolute';
labelDiv.style.width = '30px';
labelDiv.style.height = '30px';
labelDiv.style.borderRadius = '50%';
labelDiv.style.backgroundColor = '#ff6600a1';
labelDiv.style.display = 'flex';
labelDiv.style.alignItems = 'center';
labelDiv.style.justifyContent = 'center';
labelDiv.style.color = 'white';
labelDiv.style.fontSize = '12px';
labelDiv.style.fontWeight = 'bold';
labelDiv.style.pointerEvents = 'none';
labelDiv.textContent = Math.round(lengthMeters);
fragment.appendChild(labelDiv);
segmentLabelCache.push({
lon: midCoords[0],
lat: midCoords[1],
labelDiv: labelDiv,
offsetX: 15,
offsetY: 35,
});
}
}
}
} catch (err) {
// Silent error handling
}
});
// Batch append all elements to the DOM
segmentLengthContainer.appendChild(fragment);
// Update positions after creating labels
updateSegmentLabelPositions();
// Get final counts
issueCount = segmentsWithIssues.size; // Number of segments with geometry node issues
// Update badge count and icon color for bug icon (geometry nodes near endpoints only - 📍 pin icon)
if (countBadge) {
if (issueCount > 0 && options.checkGeometryIssues) {
countBadge.value = issueCount;
countBadge.style.display = 'inline-flex';
// Update bug icon color to red when issues found
const bugIcon = document.getElementById('ezroad-bug-icon');
if (bugIcon) bugIcon.style.color = '#ff3333ff';
} else {
countBadge.style.display = 'none';
// Update bug icon color to default blue when no issues
const bugIcon = document.getElementById('ezroad-bug-icon');
if (bugIcon) bugIcon.style.color = '#33CCFF';
}
}
} catch (error) {
log('Error rebuilding segment length display: ' + error.message);
}
}
// Fast position update - only updates pixel positions of existing labels
function updateSegmentLabelPositions() {
if (!segmentLengthContainer || segmentLabelCache.length === 0) {
return;
}
try {
segmentLabelCache.forEach((cached) => {
const pixel = wmeSDK.Map.getMapPixelFromLonLat({
lonLat: { lon: cached.lon, lat: cached.lat },
});
if (pixel && typeof pixel.x === 'number' && typeof pixel.y === 'number') {
const offX = cached.offsetX || 15;
const offY = cached.offsetY || 35;
cached.labelDiv.style.left = pixel.x - offX + 'px';
cached.labelDiv.style.top = pixel.y - offY + 'px';
}
});
} catch (err) {
// Silent error handling
}
}
// Poll for map changes and update display
function checkAndUpdate() {
// Do not update if map is currently moving
if (typeof isMapMoving !== 'undefined' && isMapMoving) return;
const options = getOptions();
if (!options.showSegmentLength && !options.checkGeometryIssues) {
if (segmentLengthContainer) segmentLengthContainer.style.display = 'none';
return;
} else {
if (segmentLengthContainer) segmentLengthContainer.style.display = 'block';
}
try {
let extent;
let currentZoom;
try {
extent = wmeSDK.Map.getMapExtent();
currentZoom = wmeSDK.Map.getZoomLevel();
} catch (e) {
return;
}
const currentBounds = {
west: extent[0],
south: extent[1],
east: extent[2],
north: extent[3],
};
// Check if map has moved or zoomed
if (!lastBounds || lastBounds.north !== currentBounds.north || lastBounds.south !== currentBounds.south || lastBounds.east !== currentBounds.east || lastBounds.west !== currentBounds.west || lastZoom !== currentZoom) {
lastBounds = currentBounds;
lastZoom = currentZoom;
rebuildSegmentLengthDisplay();
}
} catch (e) {}
}
function handleSegmentLengthToggle() {
const options = getOptions();
// Update button visibility immediately on toggle
addGeometryFixButton();
if (options.showSegmentLength || options.checkGeometryIssues) {
if (segmentLengthContainer) segmentLengthContainer.style.display = 'block';
// Ensure polling is active
if (!updateInterval) {
updateInterval = setInterval(checkAndUpdate, 500);
}
rebuildSegmentLengthDisplay();
} else {
if (segmentLengthContainer) segmentLengthContainer.style.display = 'none';
clearSegmentLengthDisplay();
// Stop polling
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
}
function initSegmentLengthLayer() {
log('Initializing segment length display layer');
// Find viewport div - prefer standard class or ID
// Try SDK method first if possible, though getMapViewportElement might not be exposed on all versions?
// Docs say getMapViewportElement() returns HTMLElement.
let viewportDiv;
try {
viewportDiv = wmeSDK.Map.getMapViewportElement();
} catch (e) {
log('SDK getMapViewportElement failed, trying fallbacks');
}
if (!viewportDiv) {
viewportDiv = document.querySelector('.ol-viewport') || document.querySelector('#WazeMap') || document.querySelector('#map');
}
if (!viewportDiv) {
log('Map viewport not found');
return;
}
// Create div container for length labels
segmentLengthContainer = document.createElement('div');
segmentLengthContainer.id = 'ezroad-segment-length-container';
segmentLengthContainer.style.position = 'absolute';
segmentLengthContainer.style.top = '0';
segmentLengthContainer.style.left = '0';
segmentLengthContainer.style.width = '100%';
segmentLengthContainer.style.height = '100%';
segmentLengthContainer.style.pointerEvents = 'none';
segmentLengthContainer.style.zIndex = '1000';
segmentLengthContainer.style.display = 'none'; // Hidden by default
viewportDiv.appendChild(segmentLengthContainer);
log('Container appended to map viewport');
// Variables are now defined in outer scope to allow access from handleSegmentLengthToggle
// lastBounds, lastZoom, updateInterval, isMapMoving, updateFrameRequest
// Event handlers for map movement using SDK events
const onMapMove = function () {
isMapMoving = true;
// Use requestAnimationFrame to throttle position updates during map movement
if (updateFrameRequest) {
return; // Already scheduled
}
updateFrameRequest = requestAnimationFrame(() => {
updateFrameRequest = null;
const options = getOptions();
if ((options.showSegmentLength || options.checkGeometryIssues) && segmentLengthContainer && segmentLengthContainer.style.display !== 'none') {
updateSegmentLabelPositions(); // Fast position update only
}
});
};
const onMoveEnd = function () {
isMapMoving = false;
// Rebuild labels after movement ends (checks if segments entered/left viewport)
const options = getOptions();
if ((options.showSegmentLength || options.checkGeometryIssues) && segmentLengthContainer) {
segmentLengthContainer.style.display = 'block';
rebuildSegmentLengthDisplay();
// Update lastBounds/Zoom to prevent redundant update from interval
try {
let extent = wmeSDK.Map.getMapExtent();
lastBounds = {
west: extent[0],
south: extent[1],
east: extent[2],
north: extent[3],
};
lastZoom = wmeSDK.Map.getZoomLevel();
} catch (e) {}
}
};
const onZoomChanged = function () {
const options = getOptions();
if ((options.showSegmentLength || options.checkGeometryIssues) && segmentLengthContainer) {
rebuildSegmentLengthDisplay(); // Full rebuild on zoom
try {
let extent = wmeSDK.Map.getMapExtent();
lastBounds = {
west: extent[0],
south: extent[1],
east: extent[2],
north: extent[3],
};
lastZoom = wmeSDK.Map.getZoomLevel();
} catch (e) {}
}
};
// Register SDK event listeners
wmeSDK.Events.on({
eventName: 'wme-map-move',
eventHandler: onMapMove,
});
wmeSDK.Events.on({
eventName: 'wme-map-move-end',
eventHandler: onMoveEnd,
});
wmeSDK.Events.on({
eventName: 'wme-map-zoom-changed',
eventHandler: onZoomChanged,
});
// Initialize polling if already enabled
const options = getOptions();
if (options.showSegmentLength || options.checkGeometryIssues) {
handleSegmentLengthToggle();
}
log('Segment length layer initialized');
}
// ===== End Segment Length Display Functionality =====
// 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(`[${scriptName}] 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(`[${scriptName}] 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(`[${scriptName}] Registered shortcut with no key due to conflict.`);
} catch (e2) {
console.error(`[${scriptName}] Failed to register shortcut with no key:`, e2);
}
const options = getOptions();
options.shortcutKey = null;
saveOptions(options);
}
}
// ===== New Feature: One-click Geometry Fix =====
async function fixVisibleGeometryIssues() {
const options = getOptions();
if (!options.checkGeometryIssues) {
if (WazeToastr?.Alerts) WazeToastr.Alerts.info(`${scriptName}`, 'Please enable "Check Geometry issues" in settings first.');
else alert('Please enable "Check Geometry issues" in EZRoad Mod settings first.');
return;
}
const allSegments = wmeSDK.DataModel.Segments.getAll();
let extent = wmeSDK.Map.getMapExtent();
if (!extent) return;
const mapBounds = { west: extent[0], south: extent[1], east: extent[2], north: extent[3] };
let fixedCount = 0;
let errors = 0;
let totalIssueCount = 0;
// Filter for segments with issues in view
const segmentsToFix = allSegments.filter((segment) => {
if (!segment.geometry) return false;
// Skip roundabouts (segments that are part of a junction)
if (segment.junctionId !== null) return false;
// Check for nodes too close to endpoints
const result = checkGeometryNodePlacement(segment, options.geometryIssueThreshold);
if (!result.hasIssue) return false;
// Initial visibility check of issues
const visibleIssues = result.details.filter((issue) => issue.coordinates[0] >= mapBounds.west && issue.coordinates[0] <= mapBounds.east && issue.coordinates[1] >= mapBounds.south && issue.coordinates[1] <= mapBounds.north);
if (visibleIssues.length > 0) {
totalIssueCount += visibleIssues.length;
return true;
}
return false;
});
if (segmentsToFix.length === 0) {
if (WazeToastr?.Alerts) WazeToastr.Alerts.error(`${scriptName}`, 'No visible geometry issues to fix.');
else alert('No visible geometry issues to fix.');
return;
}
const performFix = async () => {
let fixedIssueCount = 0;
for (const segment of segmentsToFix) {
try {
const result = checkGeometryNodePlacement(segment, options.geometryIssueThreshold); // Recalculate to be safe
if (!result.hasIssue) continue;
const idxToRemove = new Set(result.details.map((d) => d.nodeIndex));
const oldCoords = segment.geometry.coordinates;
const newCoords = oldCoords.filter((_, i) => !idxToRemove.has(i));
if (newCoords.length < 2) continue; // Should not happen for geometry nodes, but safety
await wmeSDK.DataModel.Segments.updateSegment({
segmentId: segment.id,
geometry: {
type: 'LineString',
coordinates: newCoords,
},
});
fixedCount++;
fixedIssueCount += result.details.length;
} catch (e) {
console.error('Failed to fix segment', segment.id, e);
errors++;
}
}
const msg = fixedIssueCount === fixedCount ? `Fixed ${fixedCount} segments.${errors > 0 ? ` (${errors} errors)` : ''}` : `Fixed ${fixedCount} segments with ${fixedIssueCount} geometry node issues.${errors > 0 ? ` (${errors} errors)` : ''}`;
if (WazeToastr?.Alerts) WazeToastr.Alerts.success(`${scriptName}`, msg);
else alert(msg);
// Refresh display
rebuildSegmentLengthDisplay();
};
const confirmMsg = totalIssueCount === segmentsToFix.length ? `Found ${segmentsToFix.length} segments with geometry issues. Fix them now?` : `Found ${segmentsToFix.length} segments with ${totalIssueCount} geometry node issues. Fix them now?`;
if (WazeToastr?.Alerts?.confirm) {
WazeToastr.Alerts.confirm(`${scriptName}`, confirmMsg, performFix, null, 'Fix', 'Cancel');
} else if (confirm(confirmMsg)) {
performFix();
}
}
function addGeometryFixButton() {
const options = getOptions();
// Check user rank - only show for L3 and above (rank >= 2 in SDK)
const userInfo = wmeSDK.State.getUserInfo();
if (!userInfo || userInfo.rank < UserRankRequiredForGeometryFix - 1) {
// Remove button if it exists and user doesn't have permission
const existingBugBtn = document.getElementById('ezroad-fix-geometry-btn');
if (existingBugBtn) existingBugBtn.remove();
return;
}
const prefsItem = document.querySelector('wz-navigation-item[data-for="prefs"]');
let bugBtn = document.getElementById('ezroad-fix-geometry-btn');
if (bugBtn) {
// Update visibility based on option and rank
bugBtn.style.display = options.checkGeometryIssues ? 'block' : 'none';
return;
}
if (!prefsItem) return;
bugBtn = document.createElement('wz-button');
bugBtn.color = 'text';
bugBtn.size = 'sm';
bugBtn.style.margin = '20px auto 0 auto';
bugBtn.id = 'ezroad-fix-geometry-btn';
bugBtn.type = 'button';
// Initial visibility based on option
bugBtn.style.display = options.checkGeometryIssues ? 'block' : 'none';
// HTML content matching user request style
bugBtn.innerHTML = `
<i class="w-icon w-icon-bug-fill" id="ezroad-bug-icon" style="color: #33CCFF" title="Auto-fix geometry nodes near endpoints"></i>
<wz-notification-indicator value="0" id="ezroad-geometry-error-count" class="counter" style="display: none;"></wz-notification-indicator>
`;
bugBtn.addEventListener('click', fixVisibleGeometryIssues);
// Insert after prefs
prefsItem.insertAdjacentElement('afterend', bugBtn);
}
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(`[${scriptName}] 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) {
try {
const city = wmeSDK.DataModel.Cities.getById({ cityId });
// Ensure city is fully loaded before accessing name
cityName = city && city.name !== undefined ? city.name : '';
} catch (e) {
log(`[${scriptName}] Error getting city name for cityId ${cityId}: ${e}`);
cityName = '';
}
}
alertMessageParts.push(`City Name: <b>${cityName || 'None'}</b>`);
}
// Helper: Returns true if the roadType is non-routable (Footpath, Pedestrianised Area, Stairway, Ferry, Railway, Runway)
// According to WME SDK, non-routable segments should have routingRoadType === null
// This function provides a fallback check based on roadType values
function isPedestrianType(roadType) {
// Footpath (5), Pedestrianised Area (10), Stairway (16), Ferry (15), Railway (18), Runway (19)
return [5, 10, 16, 15, 18, 19].includes(roadType);
}
// Helper: Enable all turns at both nodes of a segment for routable road types
function enableAllTurnsForSegment(segmentId) {
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId });
if (!seg || isPedestrianType(seg.roadType)) {
log(`[${scriptName}] Skipping turn enablement for non-routable segment ${segmentId}`);
return;
}
const nodes = [seg.fromNodeId, seg.toNodeId].filter(nodeId => nodeId !== null);
nodes.forEach(nodeId => {
try {
// Check if we can edit turns at this node
if (!wmeSDK.DataModel.Turns.canEditTurnsThroughNode({ nodeId })) {
log(`[${scriptName}] Cannot edit turns at node ${nodeId}`);
return;
}
// Get all turns through the node
const turns = wmeSDK.DataModel.Turns.getTurnsThroughNode({ nodeId });
// Enable all turns that aren't already allowed
turns.forEach(turn => {
try {
if (!turn.isAllowed) {
wmeSDK.DataModel.Turns.updateTurn({
turnId: turn.id,
isAllowed: true
});
log(`[${scriptName}] Enabled turn ${turn.id} at node ${nodeId}`);
}
} catch (turnError) {
log(`[${scriptName}] Could not enable turn ${turn.id}: ${turnError.message}`);
}
});
} catch (nodeError) {
log(`[${scriptName}] Error processing turns at node ${nodeId}: ${nodeError.message}`);
}
});
log(`[${scriptName}] Completed turn enablement for segment ${segmentId}`);
} catch (error) {
console.error(`[${scriptName}] Error enabling turns for segment ${segmentId}:`, error);
}
}
// 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) {
log(`[${scriptName}] Segment ${segmentId} not found`);
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?';
// Define the recreation logic as a function to avoid duplication
const performRecreation = () => {
try {
// Save geometry and address
const geometry = seg.geometry;
const oldPrimaryStreetId = seg.primaryStreetId;
const oldAltStreetIds = Array.isArray(seg.alternateStreetIds) ? seg.alternateStreetIds : [];
log(`[${scriptName}] Deleting segment ${segmentId} for road type conversion`);
// Delete old segment
try {
wmeSDK.DataModel.Segments.deleteSegment({ segmentId });
} catch (ex) {
const errorMsg = 'Segment could not be deleted. Please check for restrictions or junctions.';
log(`[${scriptName}] Delete failed: ${ex.message}`);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error(scriptName, errorMsg);
} else {
alert(errorMsg);
}
return null;
}
// Create new segment
log(`[${scriptName}] Creating new segment with road type ${targetRoadType}`);
const newSegmentId = wmeSDK.DataModel.Segments.addSegment({ geometry, roadType: targetRoadType });
if (!newSegmentId) {
log(`[${scriptName}] Failed to create new segment`);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error(scriptName, 'Failed to create new segment');
}
return null;
}
// 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
log(`[${scriptName}] Restoring address for new segment ${newSegmentId}`);
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) {
log(`[${scriptName}] Applying connected segment name data`);
wmeSDK.DataModel.Segments.updateAddress({
segmentId: newSegmentId,
primaryStreetId: copyConnectedNameData.primaryStreetId,
alternateStreetIds: Array.isArray(copyConnectedNameData.alternateStreetIds) ? copyConnectedNameData.alternateStreetIds : [],
});
}
// Reselect new segment
wmeSDK.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } });
// If converting from pedestrian to routable, enable all turns
if (currentIsPed && !targetIsPed) {
// Use setTimeout to ensure segment is fully created before enabling turns
setTimeout(() => {
log(`[${scriptName}] Enabling turns after conversion from pedestrian to routable type`);
enableAllTurnsForSegment(newSegmentId);
}, 300);
}
log(`[${scriptName}] Successfully recreated segment: ${segmentId} -> ${newSegmentId}`);
// Show success message
const successMsg = currentIsPed
? 'Segment successfully converted from Pedestrian type to regular street type!'
: 'Segment successfully converted to Pedestrian type!';
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(scriptName, successMsg, false, false, 3000);
} else {
alert(`${scriptName}: ${successMsg}`);
}
return newSegmentId;
} catch (error) {
log(`[${scriptName}] Error during segment recreation: ${error.message}`);
console.error(`[${scriptName}] Segment recreation error:`, error);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error(scriptName, `Error recreating segment: ${error.message}`);
} else {
alert(`${scriptName}: Error recreating segment: ${error.message}`);
}
return null;
}
};
// Show confirmation dialog
if (WazeToastr?.Alerts?.confirm) {
WazeToastr.Alerts.confirm(
scriptName,
swapMsg,
performRecreation, // OK callback - perform the recreation
() => {
// User cancelled
log(`[${scriptName}] Segment recreation cancelled by user`);
},
'Continue',
'Cancel'
);
return undefined; // Return undefined to indicate async dialog is pending
} else if (!window.confirm(swapMsg)) {
log(`[${scriptName}] Segment recreation cancelled by user`);
return null; // Cancel operation
}
// For window.confirm (synchronous), perform recreation immediately
return performRecreation();
}
return segmentId;
}
const handleUpdate = () => {
const selection = wmeSDK.Editing.getSelection();
if (!selection || selection.objectType !== 'segment') return;
// Ensure segments are loaded before processing
try {
// Validate that segments exist and are accessible
for (let id of selection.ids) {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
if (!seg) {
log(`Segment ${id} not fully loaded, waiting...`);
// Retry after a short delay
setTimeout(() => handleUpdate(), 200);
return;
}
}
} catch (e) {
log(`[${scriptName}] Error validating segments: ${e}`);
return;
}
log(`[${scriptName}] 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;
let updatedUTurn = 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 toNode = seg.toNodeId;
const connectedSegIds = getConnectedSegmentIDs(id);
// Gather all segments connected to both nodes (excluding self)
const fromNodeSegs = connectedSegIds.map((sid) => wmeSDK.DataModel.Segments.getById({ segmentId: sid })).filter((s) => s && (s.fromNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === fromNode || s.toNodeId === toNode) && 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(`[${scriptName}] 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(`[${scriptName}] Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log(`[${scriptName}] updateAddress error: ` + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
alertMessageParts.push(`[${scriptName}] 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(`[${scriptName}] 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(`[${scriptName}] Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log(`[${scriptName}] updateAddress error in fallback: ` + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
alertMessageParts.push(`[${scriptName}] Copied all attributes from connected segment.`);
log(`[${scriptName}] Copied all attributes from connected segment (fallback, no valid street name).`);
} else {
alertMessageParts.push(`[${scriptName}] No connected segment found to copy attributes.`);
}
}
} catch (error) {
console.error(`[${scriptName}] Error copying all attributes:`, error);
}
}, 100)
);
});
Promise.all(updatePromises).then(() => {
if (alertMessageParts.length) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.info(`${scriptName}`, alertMessageParts.join('<br>'), false, false, 5000);
} else {
alert(`${scriptName} ` + alertMessageParts.join('\n'));
}
}
// --- AUTOSAVE LOGIC HERE ---
if (options.autosave) {
setTimeout(() => {
log(`[${scriptName}] Delayed Autosave starting...`);
wmeSDK.Editing.save().then(() => {
log(`[${scriptName}] Delayed Autosave completed.`);
});
}, 600);
}
});
return;
}
// Apply motorbike restriction ONCE for all selected segments (before individual updates)
let motorcycleRestrictionApplied = false;
if (options.restrictExceptMotorbike) {
log(`[${scriptName}] Applying motorbike restriction to all selected segments via UI automation...`);
applyMotorbikeOnlyRestriction(selection.ids[0]).then((result) => {
if (result === true) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(
`${scriptName}`,
`Motorbike-only restriction applied to ${selection.ids.length} segment(s) ✓`,
false,
false,
3000
);
}
} else if (result === 'not_supported') {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.warning(
`${scriptName} Motorbike Restriction - Automation Failed`,
`The UI automation could not complete. Please add manually:<br><br>` +
`<b>Steps:</b><br>` +
`1. Keep segment(s) selected<br>` +
`2. Click "Restrictions" in left panel<br>` +
`3. Click "Add new" under "2 way"<br>` +
`4. Select "Entire Segment"<br>` +
`5. Add "Vehicle type" → "Motorcycle"<br>` +
`6. Click "Add" then "Apply"`,
false,
false,
10000
);
}
}
}).catch((error) => {
console.error(`[${scriptName}] Error applying motorbike restriction:`, error);
});
motorcycleRestrictionApplied = true;
}
// Flag to track if we need to wait for async confirmation dialog
let waitingForConfirmation = false;
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 === undefined) {
// Async confirmation dialog is pending - set flag and exit forEach
waitingForConfirmation = true;
return;
}
if (!newId) return; // If failed or cancelled, skip further updates for this segment
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: <b>${selectedRoad.name}</b>`);
//updatedRoadType = true;
log(`[${scriptName}] 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(`[${scriptName}] Segment ID: ${id} already has the target road type: ${options.roadType}. Skipping update.`);
alertMessageParts.push(`Road Type: <b>${selectedRoad.name} exists. Skipping update.</b>`);
updatedRoadType = true;
} else {
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
roadType: options.roadType,
});
log(`[${scriptName}] Road type updated successfully.`);
alertMessageParts.push(`Road Type: <b>${selectedRoad.name}</b>`);
updatedRoadType = true;
} catch (error) {
console.error(`[${scriptName}] 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(`[${scriptName}] Lock level to set: ${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(`[${scriptName}] Segment ID: ${id} already has the target lock level: ${displayLockLevel}. Skipping update.`);
alertMessageParts.push(`Lock Level: <b>${displayLockLevel} exists. Skipping update.</b>`);
updatedLockLevel = true;
} else {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
lockRank: toLock,
});
alertMessageParts.push(`Lock Level: <b>${displayLockLevel}</b>`);
updatedLockLevel = true;
}
} catch (error) {
console.error(`[${scriptName}] 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: <b>${speedToSet !== null ? speedToSet : 'unset'}</b>`);
updatedSpeedLimit = true;
} else {
log(`Segment ID: ${id} already has the target speed limit: ${speedToSet}. Skipping update.`);
alertMessageParts.push(`Speed Limit: <b>${speedToSet !== null ? speedToSet : 'unset'} exists. Skipping update.</b>`);
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
try {
city = getTopCity();
// Validate city is fully loaded
if (city && city.name === undefined) {
log('Top city not fully loaded, will check connected segments');
city = null;
}
} catch (e) {
log(`Error getting top city: ${e}`);
city = null;
}
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...');
try {
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 and fully loaded
if (connectedCity && !connectedCity.isEmpty && connectedCity.name !== undefined) {
city = connectedCity;
}
} else {
log('No connected address found');
}
} catch (e) {
log(`Error getting connected segment city: ${e}`);
}
}
// 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 || '' : '';
log(`Before getStreet/addStreet: cityId=${city.id}, streetName="${streetName}"`);
street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName });
if (!street) {
log(`Street not found, creating new street with cityId=${city.id}, streetName="${streetName}"`);
street = wmeSDK.DataModel.Streets.addStreet({ streetName, cityId: city.id });
}
log(`After getStreet/addStreet: street.id=${street?.id}, street.cityId=${street?.cityId}`);
// 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);
}
});
}
log(`About to updateAddress: cityId=${city.id}, street.id=${street.id}, altStreetIds=${newAltStreetIds.join(',')}`);
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: newAltStreetIds.length > 0 ? newAltStreetIds : undefined,
});
} else {
// New/empty street fallback - use the city we already determined above (from top city or connected segments)
log(`Segment has no primary street, using determined city: ${city ? `id=${city.id}, name="${city.name}"` : 'null'}`);
street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName: '' });
if (!street) {
street = wmeSDK.DataModel.Streets.addStreet({ streetName: '', cityId: city.id });
}
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id,
alternateStreetIds: undefined,
});
}
}
log(`City Name: ${city?.name}, City ID: ${city?.id}, Street ID: ${street?.id}`);
}
// COMMENTED OUT: Old unpaved handler using DOM clicks (kept for reference/fallback)
// ============================================================================
// Replaced with WME SDK updateSegment method (see below)
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: <b>Paved (pedestrian type)</b>`);
updatedPaved = true;
}
} else {
alertMessageParts.push(`Paved Status: <b>Paved (pedestrian type, already set)</b>`);
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: <b>Unpaved</b>`);
updatedPaved = true;
}
} else {
// Already unpaved, no action needed
alertMessageParts.push(`Paved Status: <b>Unpaved (already set)</b>`);
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: <b>Paved</b>`);
updatedPaved = true;
}
} else {
// Already paved, no action needed
alertMessageParts.push(`Paved Status: <b>Paved (already set)</b>`);
updatedPaved = true;
}
}
}, 500)
); // 500ms delay for unpaved/paved toggle
// ============================================================================
// The SDK logic is flawed because the unpaved attribute is not immediately updated in the segment object after updateSegment call, so we cannot reliably check the current unpaved status before deciding to click or not. This can lead to unnecessary clicks and alerts showing "Paved (already set)" when it was actually toggled. The DOM method, while less elegant, reflects the actual state of the checkbox and avoids this issue. Therefore, we will keep the DOM click method as primary for now, and use the SDK updateSegment as a backup if the DOM elements are not found (like in compact mode). We can revisit this in the future if WME SDK provides a way to get immediate feedback on flag attribute changes.
// ============================================================================
// NEW: Updated unpaved handler using WME SDK updateSegment method
// updatePromises.push(
// delayedUpdate(() => {
// try {
// const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
// const isPedestrian = isPedestrianType(seg.roadType);
// // Determine the unpaved value based on segment type and options
// let unpavedValue = false;
// let statusMessage = '';
// if (isPedestrian) {
// // Always set as paved for pedestrian types, regardless of checkbox
// unpavedValue = false;
// statusMessage = 'Paved (pedestrian type)';
// } else if (options.unpaved) {
// // Set as unpaved if option is enabled
// unpavedValue = true;
// statusMessage = 'Unpaved';
// } else {
// // Set as paved
// unpavedValue = false;
// statusMessage = 'Paved';
// }
// // Use WME SDK updateSegment with flagAttributes
// wmeSDK.DataModel.Segments.updateSegment({
// segmentId: id,
// flagAttributes: {
// unpaved: unpavedValue
// }
// });
// alertMessageParts.push(`Paved Status: <b>${statusMessage}</b>`);
// updatedPaved = true;
// log(`Updated unpaved status via SDK: ${statusMessage} (unpaved=${unpavedValue})`);
// } catch (e) {
// log('Error updating unpaved status via SDK: ' + e);
// }
// }, 500)
// ); // 500ms delay for unpaved/paved toggle
// 3a. Copy segment name from connected segment if enabled
// =========================================================================
// TIER LOGIC for intelligent name copying:
//
// TIER 1 - Primary names MATCH:
// - Preferred tier for merging alternate names
// - Keeps selected segment's primary name unchanged
// - Intelligently adds ONLY missing alternate names from connected segment
// - Avoids duplicating names that already exist
// - Prioritizes A-side segments first, then B-side if no A-side matches
//
// TIER 2 - Primary names DIFFER:
// - Used when no TIER 1 (matching) segment is found
// - Replaces primary name with connected segment's primary name
// - Copies all alternate names from connected segment
// - Prioritizes A-side segments first, then B-side if no A-side found
//
// TIER 3 - Selected segment has NO names:
// - Used when selected segment has neither primary nor alternate names
// - Copies everything (primary + all alternates) from connected segment
// - Prioritizes A-side segments first, then B-side if no A-side found
//
// FALLBACK - No valid connected segment:
// - If no connected segment has any names, skip copying
// - No changes made to selected segment
// =========================================================================
updatePromises.push(
delayedUpdate(() => {
if (options.copySegmentName) {
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
// Per WME SDK docs: reverseDirection=false gets segments at fromNode (A side), reverseDirection=true gets segments at toNode (B side)
// This correctly handles both physical nodes and virtual nodes used by pedestrian segments
const aSideSegs = wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId: id, reverseDirection: true });
const bSideSegs = wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId: id, reverseDirection: false });
// Build segsToTry: A side first, then B side
// Both sides are considered for TIER 1 matching, and the logic prioritizes by altCount
// This allows B-side matches with more alt names to be selected over A-side matches with fewer alt names
let segsToTry = [];
if (aSideSegs.length > 0) {
segsToTry = aSideSegs.map((s) => s.id);
}
if (bSideSegs.length > 0) {
segsToTry = segsToTry.concat(bSideSegs.map((s) => s.id));
}
// Get selected segment's current names
const selectedSegStreetId = seg.primaryStreetId;
const selectedSegAltStreetIds = seg.alternateStreetIds || [];
let selectedSegStreetName = '';
let selectedSegAltNames = [];
if (selectedSegStreetId) {
try {
const selectedStreet = wmeSDK.DataModel.Streets.getById({ streetId: selectedSegStreetId });
if (selectedStreet && selectedStreet.name) {
selectedSegStreetName = selectedStreet.name;
}
} catch (e) {
log(`Error getting selected segment's primary street: ${e}`);
}
}
selectedSegAltStreetIds.forEach((altId) => {
try {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) {
selectedSegAltNames.push(altStreet.name);
}
} catch (e) {
log(`Error getting selected segment's alternate street: ${e}`);
}
});
let found = false;
log(`[copySegmentName] Starting with ${segsToTry.length} connected segments. Selected primary="${selectedSegStreetName}"`);
// First pass: Look for TIER 1 matches (primary names match)
// Collect all TIER 1 matches, then prioritize those with alt names
let tier1Matches = [];
for (let connectedSegId of segsToTry) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
if (!connectedSeg) continue;
const connectedStreetId = connectedSeg.primaryStreetId;
let connectedStreetName = '';
try {
const connectedStreet = wmeSDK.DataModel.Streets.getById({ streetId: connectedStreetId });
if (connectedStreet && connectedStreet.name === undefined && connectedStreet.cityId === undefined) {
log(`[copySegmentName] Segment ${connectedSegId}: Street not fully loaded, skipping`);
continue;
}
if (connectedStreet && connectedStreet.name) {
connectedStreetName = connectedStreet.name;
}
} catch (e) {
log(`[copySegmentName] Segment ${connectedSegId}: Error getting street: ${e}`);
continue;
}
// Check for TIER 1: Primary names match
if (selectedSegStreetName !== '' && selectedSegStreetName === connectedStreetName) {
const altCount = (connectedSeg.alternateStreetIds || []).length;
log(`[copySegmentName] Found TIER 1 match at segment ${connectedSegId}: "${connectedStreetName}" (with ${altCount} alt IDs)`);
tier1Matches.push({ segId: connectedSegId, seg: connectedSeg, altCount });
}
}
// Prioritize TIER 1 match with alt names, fall back to any TIER 1 match
let tier1Match = null;
if (tier1Matches.length > 0) {
// Sort by altCount descending, pick first (highest alt count)
tier1Matches.sort((a, b) => b.altCount - a.altCount);
tier1Match = tier1Matches[0];
log(`[copySegmentName] Selected TIER 1 match: segment ${tier1Match.segId} with ${tier1Match.altCount} alts`);
}
// If TIER 1 match found, use it
if (tier1Match) {
const connectedSeg = tier1Match.seg;
const connectedStreetId = connectedSeg.primaryStreetId;
const connectedAltStreetIds = connectedSeg.alternateStreetIds || [];
let connectedStreetName = '';
let connectedAltNames = [];
try {
const connectedStreet = wmeSDK.DataModel.Streets.getById({ streetId: connectedStreetId });
if (connectedStreet && connectedStreet.name) {
connectedStreetName = connectedStreet.name;
}
} catch (e) {
log(`[copySegmentName] Error getting tier1 street: ${e}`);
}
// Get all alternate names
log(`[copySegmentName] Connected segment has ${connectedAltStreetIds.length} alt IDs: ${connectedAltStreetIds.join(', ')}`);
connectedAltStreetIds.forEach((altId) => {
try {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) {
connectedAltNames.push({ name: altStreet.name, id: altId });
log(`[copySegmentName] Alt ID ${altId}: "${altStreet.name}"`);
} else {
log(`[copySegmentName] Alt ID ${altId}: Street not found or has no name`);
}
} catch (e) {
log(`[copySegmentName] Error loading alt ID ${altId}: ${e}`);
}
});
log(`[copySegmentName] Total alt names found: ${connectedAltNames.length}`);
log(`[copySegmentName] Selected segment currently has ${selectedSegAltStreetIds.length} alt IDs: ${selectedSegAltStreetIds.join(', ')}`);
log(`[copySegmentName] Selected segment alt names: ${selectedSegAltNames.join(', ')}`);
// TIER 1: Primary names match - intelligently merge alt names
log(`TIER 1: Primary names match ("${selectedSegStreetName}"). Merging alts.`);
const newAltIds = [];
const addedAltNames = [];
// Keep existing alt street IDs
newAltIds.push(...selectedSegAltStreetIds);
// Add missing alt IDs from connected
connectedAltStreetIds.forEach((connAltId) => {
const isAlreadyPresent = selectedSegAltStreetIds.includes(connAltId) ||
selectedSegAltNames.some(altName => {
const found = connectedAltNames.find(ca => ca.id === connAltId);
return found && found.name === altName;
});
if (!isAlreadyPresent) {
newAltIds.push(connAltId);
const altName = connectedAltNames.find(ca => ca.id === connAltId);
if (altName) addedAltNames.push(altName.name);
log(`[copySegmentName] Adding missing alt: ID ${connAltId} = "${altName ? altName.name : 'NOT FOUND'}"`);
} else {
log(`[copySegmentName] Alt ID ${connAltId} already present, skipping`);
}
});
log(`[copySegmentName] Final newAltIds: ${newAltIds.join(', ')}, addedAltNames: ${addedAltNames.join(', ')}`);
let newPrimaryStreetId = selectedSegStreetId;
let newAltStreetIds = newAltIds;
const updateMessage = addedAltNames.length > 0
? `Merged: <b>${selectedSegStreetName}</b> (Added alts: ${addedAltNames.join(', ')})`
: `Already has: <b>${selectedSegStreetName}</b> (No new alts to add)`;
// Apply the update
if (options.setStreetCity) {
const emptyCity = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
let primaryStreetInEmptyCity = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: selectedSegStreetName || '',
});
if (!primaryStreetInEmptyCity) {
primaryStreetInEmptyCity = wmeSDK.DataModel.Streets.addStreet({
streetName: selectedSegStreetName || '',
cityId: emptyCity.id,
});
}
newPrimaryStreetId = primaryStreetInEmptyCity.id;
let newAltStreetIdsInEmptyCity = [];
newAltStreetIds.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,
});
}
newAltStreetIdsInEmptyCity.push(altInEmptyCity.id);
}
});
log(`[copySegmentName] Calling updateAddress (setStreetCity=true) with primaryStreetId=${newPrimaryStreetId}, alternateStreetIds=[${newAltStreetIdsInEmptyCity.join(', ')}]`);
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: newPrimaryStreetId,
alternateStreetIds: newAltStreetIdsInEmptyCity,
});
pushCityNameAlert(emptyCity.id, alertMessageParts);
updatedCityName = true;
} else {
log(`[copySegmentName] Calling updateAddress with primaryStreetId=${newPrimaryStreetId}, alternateStreetIds=[${newAltStreetIds.join(', ')}]`);
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: newPrimaryStreetId,
alternateStreetIds: newAltStreetIds,
});
if (connectedSeg.primaryStreetId) {
const connectedPrimaryStreet = wmeSDK.DataModel.Streets.getById({ streetId: connectedSeg.primaryStreetId });
if (connectedPrimaryStreet) {
pushCityNameAlert(connectedPrimaryStreet.cityId, alertMessageParts);
updatedCityName = true;
}
}
}
alertMessageParts.push(`Copied Name: ${updateMessage}`);
updatedSegmentName = true;
found = true;
} else {
// Second pass: Collect candidates for TIER 3 (selected has no names) or TIER 2 (names differ)
// Prioritize by alt count (similar to TIER 1), so segments with more alt names are preferred
let tier3Candidates = [];
let tier2Candidates = [];
for (let connectedSegId of segsToTry) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
if (!connectedSeg) continue;
const connectedStreetId = connectedSeg.primaryStreetId;
const connectedAltStreetIds = connectedSeg.alternateStreetIds || [];
let connectedStreetName = '';
// Get connected segment's primary street name
let connectedStreet = null;
try {
connectedStreet = wmeSDK.DataModel.Streets.getById({ streetId: connectedStreetId });
if (connectedStreet && connectedStreet.name === undefined && connectedStreet.cityId === undefined) {
log(`[copySegmentName] Segment ${connectedSegId}: Street not fully loaded, skipping`);
continue;
}
if (connectedStreet && connectedStreet.name) {
connectedStreetName = connectedStreet.name;
}
} catch (e) {
log(`[copySegmentName] Segment ${connectedSegId}: Error getting street: ${e}`);
continue;
}
// Skip if connected segment has no names AND no alt street IDs
if (!connectedStreetName && connectedAltStreetIds.length === 0) {
log(`[copySegmentName] Segment ${connectedSegId}: No primary name and no alt IDs, skipping`);
continue;
}
const altCount = connectedAltStreetIds.length;
// Classify as TIER 3 or TIER 2 candidate
if (selectedSegStreetName === '' && selectedSegAltNames.length === 0) {
// TIER 3: Selected has no names
tier3Candidates.push({ segId: connectedSegId, seg: connectedSeg, altCount, streetName: connectedStreetName });
log(`[copySegmentName] TIER 3 candidate: segment ${connectedSegId} with ${altCount} alts`);
} else {
// TIER 2: Names differ
tier2Candidates.push({ segId: connectedSegId, seg: connectedSeg, altCount, streetName: connectedStreetName });
log(`[copySegmentName] TIER 2 candidate: segment ${connectedSegId} with ${altCount} alts`);
}
}
// Prioritize by alt count
let selectedCandidate = null;
if (tier3Candidates.length > 0) {
// TIER 3: Sort by alt count descending, pick the one with most alts
tier3Candidates.sort((a, b) => b.altCount - a.altCount);
selectedCandidate = tier3Candidates[0];
log(`[copySegmentName] Selected TIER 3 candidate: segment ${selectedCandidate.segId} with ${selectedCandidate.altCount} alts`);
} else if (tier2Candidates.length > 0) {
// TIER 2: Sort by alt count descending, pick the one with most alts
tier2Candidates.sort((a, b) => b.altCount - a.altCount);
selectedCandidate = tier2Candidates[0];
log(`[copySegmentName] Selected TIER 2 candidate: segment ${selectedCandidate.segId} with ${selectedCandidate.altCount} alts`);
}
// Process the selected candidate
if (selectedCandidate) {
const connectedSeg = selectedCandidate.seg;
const connectedSegId = selectedCandidate.segId;
const connectedStreetId = connectedSeg.primaryStreetId;
const connectedAltStreetIds = connectedSeg.alternateStreetIds || [];
let connectedAltNames = [];
// Get connected segment's alternate street names
log(`[copySegmentName] Connected segment ${connectedSegId} has ${connectedAltStreetIds.length} alt street IDs: ${connectedAltStreetIds.join(', ')}`);
connectedAltStreetIds.forEach((altId) => {
try {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) {
connectedAltNames.push({ name: altStreet.name, id: altId });
log(`[copySegmentName] Alt ID ${altId}: "${altStreet.name}"`);
} else {
log(`[copySegmentName] Alt ID ${altId}: Street not found or has no name yet`);
}
} catch (e) {
log(`[copySegmentName] Alt ID ${altId}: Error loading - ${e}`);
}
});
log(`[copySegmentName] Successfully loaded names for ${connectedAltNames.length}/${connectedAltStreetIds.length} alt IDs`);
let newPrimaryStreetId = selectedSegStreetId;
let newAltStreetIds = [...selectedSegAltStreetIds];
let updateMessage = '';
if (selectedSegStreetName === '' && selectedSegAltNames.length === 0) {
// TIER 3: Selected segment has NO names - copy everything from connected
log(`TIER 3: Selected has no names. Copying from segment ${connectedSegId}: "${selectedCandidate.streetName}" with ${connectedAltStreetIds.length} alt IDs`);
newPrimaryStreetId = connectedStreetId;
newAltStreetIds = connectedAltStreetIds;
const altSummary = connectedAltNames.length > 0
? `(Alts: ${connectedAltNames.map(a => a.name).join(', ')})`
: connectedAltStreetIds.length > 0
? `(${connectedAltStreetIds.length} alt IDs: ${connectedAltStreetIds.join(', ')})`
: '';
updateMessage = `Copied: <b>${selectedCandidate.streetName}</b> ${altSummary}`;
} else {
// TIER 2: Primary names differ - replace and copy all alts
log(`TIER 2: Primary names differ. Selected="${selectedSegStreetName}", Connected="${selectedCandidate.streetName}". Using connected with ${connectedAltStreetIds.length} alt IDs`);
newPrimaryStreetId = connectedStreetId;
newAltStreetIds = connectedAltStreetIds;
const altSummary = connectedAltNames.length > 0
? `(Alts: ${connectedAltNames.map(a => a.name).join(', ')})`
: connectedAltStreetIds.length > 0
? `(${connectedAltStreetIds.length} alt IDs: ${connectedAltStreetIds.join(', ')})`
: '';
updateMessage = `Replaced: <b>${selectedCandidate.streetName}</b> ${altSummary}`;
}
// Apply the address update
if (options.setStreetCity) {
const emptyCity = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
// Create/find street for primary in empty city
let primaryStreetInEmptyCity = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: selectedCandidate.streetName || '',
});
if (!primaryStreetInEmptyCity) {
primaryStreetInEmptyCity = wmeSDK.DataModel.Streets.addStreet({
streetName: selectedCandidate.streetName || '',
cityId: emptyCity.id,
});
}
newPrimaryStreetId = primaryStreetInEmptyCity.id;
// Create/find streets for alts in empty city
let newAltStreetIdsInEmptyCity = [];
newAltStreetIds.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,
});
}
newAltStreetIdsInEmptyCity.push(altInEmptyCity.id);
}
});
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: newPrimaryStreetId,
alternateStreetIds: newAltStreetIdsInEmptyCity,
});
pushCityNameAlert(emptyCity.id, alertMessageParts);
updatedCityName = true;
} else {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: newPrimaryStreetId,
alternateStreetIds: newAltStreetIds,
});
if (connectedSeg.primaryStreetId) {
const connectedPrimaryStreet = wmeSDK.DataModel.Streets.getById({ streetId: connectedSeg.primaryStreetId });
if (connectedPrimaryStreet) {
pushCityNameAlert(connectedPrimaryStreet.cityId, alertMessageParts);
updatedCityName = true;
}
}
}
alertMessageParts.push(`Copied Name: ${updateMessage}`);
updatedSegmentName = true;
found = true;
}
}
if (!found) {
alertMessageParts.push(`Copied Name: <b>None (no connected segment with valid name)</b>`);
updatedSegmentName = true;
}
} catch (error) {
console.error('Error copying segment name:', error);
}
}
}, 100)
); // Run early in the update chain
// Enable U-Turn logic: Only allow if not already allowed
// Enable U-Turn if option is checked
updatePromises.push(
delayedUpdate(() => {
// Skip U-turn updates for pedestrian type segments (non-routable)
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
if (seg && isPedestrianType(seg.roadType)) {
log(`[EZRoad] Skipping U-turn update for pedestrian type segment (roadType: ${seg.roadType})`);
return;
}
if (options.enableUTurn) {
let sideAResult = null;
let sideBResult = null;
function switchSegmentUturnHybrid(direction = 'A') {
// --- 1. Legacy W Model Method ---
if (typeof W !== 'undefined' && W.model && W.model.getTurnGraph && W.model.actionManager) {
try {
// Fix: Ensure constructor is loaded correctly
if (typeof WazeActionSetTurn !== 'function') {
const SetTurnModule = require('Waze/Model/Graph/Actions/SetTurn');
WazeActionSetTurn = SetTurnModule.default || SetTurnModule;
}
const seg = W.model.segments.getObjectById(id);
if (!seg || seg.isOneWay()) return 'skipped';
const node = direction === 'A' ? seg.getFromNode() : seg.getToNode();
// Check current state
if (seg.isTurnAllowed(seg, node)) {
log(`[EZRoad] U-turn at ${direction} already allowed.`);
return 'already';
}
const turn = W.model.getTurnGraph().getTurnThroughNode(node, seg, seg);
if (!turn) return 'failed';
W.model.actionManager.add(
new WazeActionSetTurn(
W.model.getTurnGraph(),
turn.withTurnData(turn.getTurnData().withState(1)) // 1 is ALLOW
)
);
return 'enabled';
} catch (e) {
console.error('WME_EZRoads_Mod: Legacy U-turn error:', e);
}
}
// --- 2. SDK Method Fallback ---
if (typeof wmeSDK !== 'undefined' && wmeSDK.DataModel && wmeSDK.DataModel.Turns) {
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
if (!seg || !seg.isTwoWay) return 'skipped';
const nodeId = direction === 'A' ? seg.fromNodeId : seg.toNodeId;
if (!wmeSDK.DataModel.Turns.canEditTurnsThroughNode({ nodeId })) return 'failed';
if (wmeSDK.DataModel.Turns.isTurnAllowed({ fromSegmentId: seg.id, nodeId, toSegmentId: seg.id })) {
return 'already';
}
let turns = wmeSDK.DataModel.Turns.getTurnsThroughNode({ nodeId });
turns = turns.filter(turn => turn.isUTurn && turn.fromSegmentId === seg.id && turn.toSegmentId === seg.id);
if (turns.length === 0) return 'failed';
for (let i = 0; i < turns.length; i++) {
wmeSDK.DataModel.Turns.updateTurn({ turnId: turns[i].id, isAllowed: true });
}
return 'enabled';
} catch (e) {
console.error('WME_EZRoads_Mod: SDK U-turn error:', e);
}
}
return 'failed';
}
try {
sideAResult = switchSegmentUturnHybrid('A');
sideBResult = switchSegmentUturnHybrid('B');
// Handle alert messaging based on results
if (sideAResult === 'enabled' || sideBResult === 'enabled') {
// At least one side was newly enabled
alertMessageParts.push(`U-Turn: <b>Allowed</b>`);
updatedUTurn = true;
} else if (sideAResult === 'already' && sideBResult === 'already') {
// Both sides were already enabled
alertMessageParts.push(`U-Turn: <b>Already Enabled</b>`);
updatedUTurn = true; // Mark as updated so it shows in the combined alert
} else if (sideAResult === 'already' || sideBResult === 'already') {
// One side already enabled, the other failed/skipped
alertMessageParts.push(`U-Turn: <b>Already Enabled</b>`);
updatedUTurn = true;
}
} catch (error) {
console.error('Error switching U-turn:', error);
}
}
}, 450)
);
});
// If waiting for async confirmation, exit early - don't process any updates
if (waitingForConfirmation) {
log('[EZRoad] Waiting for user confirmation, exiting handleUpdate');
return;
}
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')));
if (updatedUTurn) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('U-Turn')));
const message = updatedFeatures.filter(Boolean).join(', ');
if (message) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.info(`${scriptName}`, `Segment updated with: ${message}`, false, false, 3000);
} else {
alert(`${scriptName} Segment updated (WazeToastr Alerts not available)`);
}
}
};
// Autosave - DELAYED AUTOSAVE
if (options.autosave) {
setTimeout(() => {
log(`[${scriptName}] Delayed Autosave starting...`);
wmeSDK.Editing.save().then(() => {
log(`[${scriptName}] Delayed Autosave completed.`);
showAlert();
});
}, 600); // 1000ms (1 second) delay before autosave
} else {
showAlert();
}
// Refresh UI with updated status by reselecting the segments
if (selection && selection.ids && selection.ids.length > 0) {
wmeSDK.Editing.setSelection({ selection: { ids: selection.ids, objectType: 'segment' } });
}
});
};
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(`${scriptName}`, 'Lock Levels saved!', false, false, 1500);
} else {
alert(`${scriptName} 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(`${scriptName}`, 'Speed Values saved!', false, false, 1500);
} else {
alert(`${scriptName} 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: 'enableUTurn',
text: 'Enable U-Turn',
key: 'enableUTurn',
tooltip: 'Enables U-turn for the selected segment when Quick Update is triggered. Works for both one-way and two-way segments and add U-turns at both ends.',
},
{
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.',
},
{
id: 'showSegmentLength',
text: 'Show Segment Length <=20m',
key: 'showSegmentLength',
tooltip: 'Displays segment length in an orange circle with white font for segments 20 meters or shorter in the visible map area.',
},
{
id: 'checkGeometryIssues',
text: 'Check Geometry issues near node',
key: 'checkGeometryIssues',
tooltip: 'Checks if any intermediate geometry nodes are too close to the start or end nodes. Configure threshold distance below. Displays a pin icon if an issue is found.',
},
{
id: 'restrictExceptMotorbike',
text: 'Restrict except Motorbike (Auto)',
key: 'restrictExceptMotorbike',
tooltip: 'Automatically adds motorbike-only vehicle restrictions via UI automation. Applies to entire segment in both directions, all day. Blocks all vehicles except motorcycles. May use shortcut key (Alt+R) 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 = $(`<div class="ezroadsmod-option">
<div class="ezroadsmod-radio-container">
<input type="radio" id="${id}" name="defaultRoad" data-road-value="${roadType.value}" ${isChecked ? 'checked' : ''}>
<label for="${id}">${roadType.name}</label>
<select id="lock-level-${roadType.id}" class="road-lock-level" data-road-id="${roadType.id}" ${!localOptions.setLock ? 'disabled' : ''}>
${locks.map((lock) => `<option value="${lock.value}" ${lockSetting.lock === lock.value ? 'selected' : ''}>${lock.value === 'HRCS' ? 'HRCS' : 'L' + lock.value}</option>`).join('')}
</select>
<input type="number" id="speed-${roadType.id}" class="road-speed" data-road-id="${roadType.id}"
value="${speedSetting.speed}" min="-1" ${!localOptions.updateSpeed ? 'disabled' : ''}>
</div>
</div>`);
div.find('input[type="radio"]').on('click', () => {
update('roadType', roadType.value);
currentRoadType = roadType.value;
});
div.find('select').on('change', function () {
updateLockLevel(roadType.id, $(this).val());
});
div.find('input.road-speed').on('change', function () {
// Get the value as a number
const speedValue = parseInt($(this).val(), 10);
// If it's not a number, reset to 0
if (isNaN(speedValue)) {
$(this).val(0);
updateSpeed(roadType.id, 0);
} else {
updateSpeed(roadType.id, speedValue);
}
});
return div;
};
// Helper function to create checkboxes
const createCheckbox = (option) => {
const isChecked = localOptions[option.key];
const otherClass = option.key !== 'autosave' && option.key !== 'copySegmentAttributes' && option.key !== 'showSegmentLength' && option.key !== 'checkGeometryIssues' && option.key !== 'restrictExceptMotorbike' ? 'ezroadsmod-other-checkbox' : '';
const attrClass = option.key === 'copySegmentAttributes' ? 'ezroadsmod-attr-checkbox' : '';
const div = $(`<div class="ezroadsmod-option">
<input type="checkbox" id="${option.id}" name="${option.id}" class="${otherClass} ${attrClass}" ${isChecked ? 'checked' : ''} title="${option.tooltip || ''}">
<label for="${option.id}" title="${option.tooltip || ''}">${option.text}</label>
</div>`);
div.on('click', () => {
// Mutually exclusive logic for setStreet and copySegmentName
if (option.key === 'setStreet' && $(`#${option.id}`).prop('checked')) {
$('#copySegmentName').prop('checked', false);
update('copySegmentName', false);
}
if (option.key === 'copySegmentName' && $(`#${option.id}`).prop('checked')) {
$('#setStreet').prop('checked', false);
update('setStreet', false);
}
// Mutual exclusion logic for copySegmentAttributes and other checkboxes
if (option.key === 'copySegmentAttributes') {
if ($(`#${option.id}`).prop('checked')) {
// Uncheck all other checkboxes except autosave
$('.ezroadsmod-other-checkbox').each(function () {
$(this).prop('checked', false);
const key = $(this).attr('id');
update(key, false);
});
update('copySegmentAttributes', true);
} else {
update('copySegmentAttributes', false);
}
} else if (option.key !== 'autosave' && option.key !== 'showSegmentLength' && option.key !== 'checkGeometryIssues' && option.key !== 'restrictExceptMotorbike') {
// If any other checkbox (except autosave, showSegmentLength, checkGeometryIssues, restrictExceptMotorbike) is checked, uncheck copySegmentAttributes
if ($(`#${option.id}`).prop('checked')) {
$('#copySegmentAttributes').prop('checked', false);
update('copySegmentAttributes', false);
}
update(option.key, $(`#${option.id}`).prop('checked'));
} else {
// Autosave, showSegmentLength, checkGeometryIssues, or restrictExceptMotorbike
update(option.key, $(`#${option.id}`).prop('checked'));
}
// Handle Segment Length / Geometry Check toggle
if (option.key === 'showSegmentLength' || option.key === 'checkGeometryIssues' || option.key === 'copySegmentAttributes') {
handleSegmentLengthToggle();
}
});
return div;
};
// -- Set up the tab for the script
wmeSDK.Sidebar.registerScriptTab().then(({ tabLabel, tabPane }) => {
tabLabel.innerText = 'EZRoads Mod';
tabLabel.title = 'Easily Update Roads';
// Setup base styles
const styles = $(`<style>
#ezroadsmod-settings {
padding-right: 10px;
box-sizing: border-box;
}
#ezroadsmod-settings * {
box-sizing: border-box;
}
#ezroadsmod-settings h2, #ezroadsmod-settings h5 {
margin-top: 0;
margin-bottom: 10px;
}
.ezroadsmod-section {
margin-bottom: 15px;
}
.ezroadsmod-option {
margin-bottom: 8px;
}
.ezroadsmod-radio-container {
display: flex;
align-items: center;
}
.ezroadsmod-radio-container input[type="radio"] {
margin-right: 5px;
}
.ezroadsmod-radio-container label {
flex: 1;
margin-right: 10px;
text-align: left;
}
.ezroadsmod-radio-container select {
width: 80px;
margin-left: auto;
margin-right: 5px;
}
.ezroadsmod-radio-container input.road-speed {
width: 50px;
margin-right: 5px;
}
.ezroadsmod-reset-button {
margin-top: 20px;
padding: 8px 12px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.ezroadsmod-reset-button:hover {
background-color: #d32f2f;
}
</style>`);
tabPane.innerHTML = '<div id="ezroadsmod-settings"></div>';
const scriptContentPane = $('#ezroadsmod-settings');
scriptContentPane.append(styles);
// Header section
const header = $(`<div class="ezroadsmod-section">
<h2>EZRoads Mod</h2>
<div>Current Version: <b>${scriptVersion}</b></div>
<div>Update Keybind: <kbd>G</kbd></div>
<div style="font-size: 0.8em;">You can change it in WME keyboard setting!</div>
</div>`);
scriptContentPane.append(header);
// Road type and options header
const roadTypeHeader = $(`<div class="ezroads-section">
<div style="display: flex; align-items: center;">
<div style="flex-grow: 1; text-align: center;">Road Type</div>
<div style="width: 80px; text-align: center;">Lock</div>
<div style="width: 60px; text-align: center;">Speed</div>
</div>
</div>`);
scriptContentPane.append(roadTypeHeader);
// Road type section with header
const roadTypeSection = $(`<div class="ezroads-section">
<div id="road-type-options"></div>
</div>`);
scriptContentPane.append(roadTypeSection);
const roadTypeOptions = roadTypeSection.find('#road-type-options');
roadTypes.forEach((roadType) => {
roadTypeOptions.append(createRadioButton(roadType));
});
// Additional options section
const additionalSection = $(`<div class="ezroadsmod-section">
<h5>Additional Options</h5>
<div id="additional-options"></div>
</div>`);
scriptContentPane.append(additionalSection);
const additionalOptions = additionalSection.find('#additional-options');
checkboxOptions.forEach((option) => {
additionalOptions.append(createCheckbox(option));
// Add threshold input right after the checkGeometryIssues checkbox
if (option.key === 'checkGeometryIssues') {
const geometryThresholdDiv = $(`<div class="ezroadsmod-option" style="margin-left: 20px; margin-top: -5px;">
<label for="geometryIssueThreshold" style="font-size: 0.9em;">Threshold distance (meters):</label>
<input type="number" id="geometryIssueThreshold" min="0.1" max="10" step="0.1" value="${
localOptions.geometryIssueThreshold || 2
}" style="width: 60px; margin-left: 5px;" title="Distance in meters to check for geometry nodes near segment endpoints">
</div>`);
additionalOptions.append(geometryThresholdDiv);
}
});
// Handle geometry threshold input change
$(document).on('change', '#geometryIssueThreshold', function () {
let thresholdValue = parseFloat($(this).val());
if (isNaN(thresholdValue) || thresholdValue < 0.1) {
thresholdValue = 2;
$(this).val(2);
} else if (thresholdValue > 10) {
thresholdValue = 10;
$(this).val(10);
}
update('geometryIssueThreshold', thresholdValue);
// Refresh display if geometry check is enabled
if (localOptions.checkGeometryIssues) {
rebuildSegmentLengthDisplay();
}
});
// Update all lock dropdowns when setLock checkbox changes
$(document).on('click', '#setLock', function () {
const isChecked = $(this).prop('checked');
$('.road-lock-level').prop('disabled', !isChecked);
});
// Update all speed inputs when updateSpeed checkbox changes
$(document).on('click', '#updateSpeed', function () {
const isChecked = $(this).prop('checked');
$('.road-speed').prop('disabled', !isChecked);
log('Speed update option changed to: ' + isChecked);
});
// Reset button section
const resetButton = $(`<button class="ezroadsmod-reset-button">Reset All Options</button>`);
resetButton.on('click', function () {
if (confirm('Are you sure you want to reset all options to default values? It will reload the webpage!')) {
resetOptions();
}
});
scriptContentPane.append(resetButton);
// --- Export/Import Config UI ---
const exportImportSection = $(
`<div class="ezroadsmod-section" style="margin-top:10px;">
<div style="display:flex; align-items:center; gap:5px; margin-bottom:5px;">
<button id="ezroadsmod-export-btn" style="font-size:0.9em;padding:4px 8px;white-space:nowrap;">Export Lock/Speed Config</button>
<button id="ezroadsmod-import-btn" style="font-size:0.9em;padding:4px 8px;white-space:nowrap;">Import Lock/Speed Config</button>
</div>
<input id="ezroadsmod-import-input" type="text" placeholder="Paste config here" style="width:95%;font-size:0.9em;padding:3px;">
</div>`
);
scriptContentPane.append(exportImportSection);
// Export logic
$(document).on('click', '#ezroadsmod-export-btn', function () {
const options = getOptions();
const presets = getCustomPresets();
const exportData = {
locks: options.locks,
speeds: options.speeds,
customPresets: presets,
};
const exportStr = JSON.stringify(exportData, null, 2);
// Copy to clipboard
navigator.clipboard.writeText(exportStr).then(
() => {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(`${scriptName}`, 'Lock/Speed config with all presets copied to clipboard!', false, false, 2000);
} else {
alert(`${scriptName} Lock/Speed config with all presets copied to clipboard!`);
}
},
() => {
alert(`${scriptName} Failed to copy config to clipboard.`);
}
);
});
// Import logic
$(document).on('click', '#ezroadsmod-import-btn', function () {
const importStr = $('#ezroadsmod-import-input').val();
if (!importStr) {
alert(`[${scriptName}] Please paste a config string to import.`);
return;
}
let importData;
try {
importData = JSON.parse(importStr);
} catch (e) {
alert(`[${scriptName}] Invalid config string!`);
return;
}
if (importData.locks && importData.speeds) {
const options = getOptions();
options.locks = importData.locks;
options.speeds = importData.speeds;
saveOptions(options);
// Update in-memory localOptions and UI
localOptions.locks = importData.locks;
localOptions.speeds = importData.speeds;
// Import custom presets if they exist
if (importData.customPresets) {
window.localStorage.setItem('WME_EZRoads_Mod_CustomPresets', JSON.stringify(importData.customPresets));
refreshPresetsList();
}
// Update lock dropdowns
$('.road-lock-level').each(function () {
const roadId = $(this).data('road-id');
const lockSetting = localOptions.locks.find((l) => l.id == roadId);
if (lockSetting) $(this).val(lockSetting.lock);
});
// Update speed inputs
$('.road-speed').each(function () {
const roadId = $(this).data('road-id');
const speedSetting = localOptions.speeds.find((s) => s.id == roadId);
if (speedSetting) $(this).val(speedSetting.speed);
});
if (WazeToastr?.Alerts) {
const presetsCount = importData.customPresets ? Object.keys(importData.customPresets).length : 0;
const message = presetsCount > 0 ? `Config imported with ${presetsCount} preset(s)!` : 'Config imported and applied!';
WazeToastr.Alerts.success(`${scriptName}`, message, false, false, 2000);
} else {
alert(`${scriptName} ${message}`);
}
} else {
alert(`${scriptName} Config missing lock/speed data!`);
}
});
// --- Custom Presets UI ---
const customPresetsSection = $(
`<div class="ezroadsmod-section" style="margin-top:15px; padding-top:12px; border-top: 1px solid #ccc;">
<div style="font-weight:bold; margin-bottom:6px; font-size:0.95em;">Custom Lock/Speed Presets</div>
<div style="margin-bottom:6px;">
<input id="ezroadsmod-preset-name" type="text" placeholder="Preset name" style="width:45%;margin-right:5px;font-size:0.9em;padding:3px;">
<button id="ezroadsmod-save-preset-btn" style="font-size:0.9em;padding:3px 8px;">Save</button>
</div>
<div id="ezroadsmod-presets-list" style="margin-top:6px;"></div>
</div>`
);
scriptContentPane.append(customPresetsSection);
// Function to refresh the presets list UI
const refreshPresetsList = () => {
const presets = getCustomPresets();
const presetsListDiv = $('#ezroadsmod-presets-list');
presetsListDiv.empty();
const presetNames = Object.keys(presets);
const currentPresetName = getCurrentPresetName();
const isModified = isCurrentPresetModified();
if (presetNames.length === 0) {
presetsListDiv.append('<div style="color:#888;font-style:italic;font-size:0.85em;">No presets saved.</div>');
return;
}
presetNames.forEach((presetName) => {
const presetData = presets[presetName];
const savedDate = presetData.savedAt ? new Date(presetData.savedAt).toLocaleDateString() : 'Unknown';
const isCurrent = currentPresetName === presetName && !isModified;
const currentIndicator = isCurrent ? '<span style="color:#4CAF50; font-weight:bold; margin-right:5px;">Current</span>' : '';
const presetDiv = $(
`<div class="ezroadsmod-preset-item" style="margin-bottom:5px; padding:5px 5px 5px 5px; background-color:rgba(128, 128, 128, 0.12); border-radius:3px;">
<div style="display:flex; align-items:center; justify-content:space-between; padding-right:10px;">
<div style="font-size:1.0em;">
<strong>${presetName}</strong>
<span style="font-size:0.85em; color: #949494ff; margin-left:5px;">(${savedDate})</span>
</div>
<div style="white-space:nowrap;">
${currentIndicator}
<button class="ezroadsmod-load-preset-btn" data-preset-name="${presetName}" style="margin-right:2px;font-size:0.9em;padding:2px 5px;">Load</button>
<button class="ezroadsmod-delete-preset-btn" data-preset-name="${presetName}" style="background-color:#f44336; color:white;font-size:0.9em;padding:3px 8px;">Delete</button>
</div>
</div>
</div>`
);
presetsListDiv.append(presetDiv);
});
};
// Initial load of presets list
refreshPresetsList();
// Add event listener for refreshing presets list when settings change
$('#ezroadsmod-presets-list').on('refresh-presets', refreshPresetsList);
// Save preset
$(document).on('click', '#ezroadsmod-save-preset-btn', function () {
const presetName = $('#ezroadsmod-preset-name').val().trim();
if (!presetName) {
alert('Please enter a preset name.');
return;
}
const presets = getCustomPresets();
if (presets[presetName]) {
if (!confirm(`Preset "${presetName}" already exists. Overwrite it?`)) {
return;
}
}
if (saveCustomPreset(presetName)) {
$('#ezroadsmod-preset-name').val('');
refreshPresetsList();
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(`${scriptName}`, `Preset "${presetName}" saved!`, false, false, 2000);
} else {
alert(`${scriptName} Preset "${presetName}" saved!`);
}
}
});
// Load preset
$(document).on('click', '.ezroadsmod-load-preset-btn', function () {
const presetName = $(this).data('preset-name');
if (loadCustomPreset(presetName)) {
// Update in-memory localOptions
const options = getOptions();
localOptions.locks = options.locks;
localOptions.speeds = options.speeds;
// Update lock dropdowns
$('.road-lock-level').each(function () {
const roadId = $(this).data('road-id');
const lockSetting = localOptions.locks.find((l) => l.id == roadId);
if (lockSetting) $(this).val(lockSetting.lock);
});
// Update speed inputs
$('.road-speed').each(function () {
const roadId = $(this).data('road-id');
const speedSetting = localOptions.speeds.find((s) => s.id == roadId);
if (speedSetting) $(this).val(speedSetting.speed);
});
// Refresh the presets list to show the current indicator
refreshPresetsList();
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(`${scriptName}`, `Preset "${presetName}" loaded!`, false, false, 2000);
} else {
alert(`${scriptName} Preset "${presetName}" loaded!`);
}
} else {
alert(`${scriptName} Failed to load preset "${presetName}".`);
}
});
// Delete preset
$(document).on('click', '.ezroadsmod-delete-preset-btn', function () {
const presetName = $(this).data('preset-name');
if (!confirm(`Are you sure you want to delete preset "${presetName}"?`)) {
return;
}
if (deleteCustomPreset(presetName)) {
refreshPresetsList();
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success(`${scriptName}`, `Preset "${presetName}" deleted!`, false, false, 2000);
} else {
alert(`${scriptName} Preset "${presetName}" deleted!`);
}
} else {
alert(`${scriptName} Failed to delete preset "${presetName}".`);
}
});
});
};
function scriptupdatemonitor() {
if (WazeToastr?.Ready) {
// Create and start the ScriptUpdateMonitor
const updateMonitor = new WazeToastr.Alerts.ScriptUpdateMonitor(scriptName, scriptVersion, downloadUrl, GM_xmlhttpRequest);
updateMonitor.start(2, true); // Check every 2 hours, check immediately
// Show the update dialog for the current version
WazeToastr.Interface.ShowScriptUpdate(scriptName, scriptVersion, updateMessage, downloadUrl, forumURL);
} else {
setTimeout(scriptupdatemonitor, 250);
}
}
scriptupdatemonitor();
console.log(`${scriptName} initialized.`);
// Custom code to run after WME bootstrap for legacy require calls for UTurn action. without it WazeActionSetTurn is undefined.
let WazeActionSetTurn
$(document).on('bootstrap.wme', () => {
// Require Waze components with a check for .default (ES6 modules)
const SetTurnModule = require('Waze/Model/Graph/Actions/SetTurn');
WazeActionSetTurn = SetTurnModule.default || SetTurnModule;
});
// Fallback: If bootstrap already happened, try to require it now
if (typeof require !== 'undefined') {
try {
const SetTurnModule = require('Waze/Model/Graph/Actions/SetTurn');
WazeActionSetTurn = SetTurnModule.default || SetTurnModule;
} catch (e) {
console.warn('EZRoads Mod: Could not load WazeActionSetTurn immediately.');
}
}
/*
Changelog
Version 2.6.8.6 - 2026-02-20:</strong><br>
- Restored paved or unpaved function to DOM since SDK methods do not provide immediate feedback.<br>
Version 2.6.8.3 - 2026-02-20:</strong><br>
- Added shortcuts support for toggling additional options.\n This is temporary fix using legacy method for saving keys between sessions.<br>
- Migrated unpaved status handling to new SDK methods.<br>
- Migrated copying of flag attributes to new SDK methods.<br>
- Updated logic for copying connected segment name and city to use new SDK methods and added more robust handling for finding connected segments with valid city.<br>
- Fixed found bug fixes.<br>
Version 2.6.8.1 - 2026-02-18
- Fixed an issue with copying segment name from connected segment. It will prioritize copying from the connected segment Side A that has a valid city name when "Set city as none" is unchecked.
Version 2.6.7.9 - 2024-06-09
- Fixed issue with converting the segment to pedestrian type and vice versa<br>
- Added direct shortcut key to update motorcycle restriction (Alt+R) <br>
- Improved alert message when motorbike restriction cannot be applied due to segment type
Version 2.6.7.8 - 2026-02-09
- Added direct shortcut key to update motorcycle restriction (Alt+R)
- Improved alert message when motorbike restriction cannot be applied due to segment type
Version 2.6.7.7 - 2026-02-09
- Added direct shortcut key to update motorcycle restriction (Alt+R)
Version 2.6.7.6 - 2026-02-08
- Improved non-routable segment detection: now properly skips "enable uturn" for all non-routable segments (Ferry, Railway, Runway, Footpath, Pedestrianised Area, Stairway) by checking routingRoadType from WME SDK
- Added restriction to allow only Motorbikes via UI automation for both one way and two way segments. SDK does not support vehicle restrictions yet.
Version 2.6.7.3 - 2026-01-28
- Fixed an issue with copying city names or segment names
2.6.7.2 - 2026-01-23
- Fixed an issue where "Enable Uturn" was disabling existing U-turns.
2.6.7.1 - 2026-01-23:
- Added checkbox for enabling U-turns.<br>
When checked, it will add U-turns to the selected road segments' both sides<br> and when pressed again, it will remove U-turns from both sides.
(thanks to Tahshee for the suggestion).
2.6.6 - 2026-01-09
- Roundabouts (segments with junctionId) are now excluded from geometry issue detection and fixing.
- Added user-configurable threshold distance input field for geometry checks:
- Range: 0.1 to 10 meters
- Step: 0.1 meter increments
- Default: 2 meters
- Users can now customize the distance threshold for detecting geometry nodes too close to segment endpoints.
- Fixed geometry issue marker (pin icon) positioning to prevent cropping when using higher threshold values.
- Consolidated duplicate geometry threshold constants into single configurable option.
- Improved settings UI with threshold input field directly below the "Check Geometry issues" checkbox.
- Threshold changes immediately refresh the map display when geometry checking is enabled.
- Fixed issue where auto setting up segment city from connected segments could fail in some cases.
2.6.5 - 2026-01-07
- Enhanced geometry fix icon: Bug icon changes color to red when geometry issues are detected, blue when no issues found.
- Improved geometry fix confirmation messages to show total count of geometry node issues.
- Fixed geometry fix success message to accurately report both segment count and geometry node count.
- Better visual feedback for geometry quality checking status.
2.6.4 - 2026-01-07
- Fixed checkbox mutual exclusion logic for display features.
- "Show Segment Length ≤20m" and "Check Geometry issues near node" checkboxes now work independently from the "Copy Connected Segment Attribute" option.
- These display options can now be enabled/disabled without affecting or being affected by the Copy Connected Segment Attribute feature.
- Improved user interface behavior and option interactions.
2.6.3 - 2026-01-07
- Added "Show Segment Length ≤20m" feature: Displays segment length in an orange circle overlay for segments 20 meters or shorter.
- Added "Check Geometry issues near node" feature: Detects intermediate geometry nodes that are too close (within 2m) to segment start/end nodes.
- Geometry issues are marked with pin (📍) icons on the map.
- Added one-click geometry fix button (bug icon) in the navigation bar to automatically fix all visible geometry issues (requires L3+ rank).
- Added notification badge showing count of geometry issues in the current view.
- Integrated Turf.js library for accurate geometry calculations and distance measurements.
- New settings toggles for both segment length display and geometry quality checking.
- Optimized performance with efficient map viewport tracking and label caching.
- Fixed various minor bugs and improved code stability.
2.6.2 - 2026-01-05
- 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.
2.6.1 - 2025-12-29
- Enhanced "Copy Connected Segment Attribute" feature:
- Now copies all segment attributes including:
- Direction (one-way A→B, B→A, or two-way)
- Forward and reverse speed limits
- Road type
- Lock rank
- Elevation level
- Primary and alternate street names
- Unpaved status
2.6.0.0 - 2025-12-28
- Added "Current" preset indicator: Shows which preset is currently loaded with a green badge.
- Current indicator disappears when settings are modified, reappears when saved back to the same preset.
- Fixed speed limit removal: Setting speed to 0 or -1 now properly removes speed limits (uses null instead of undefined).
- Improved speed limit update logic with better comparison handling.
- Preset list automatically refreshes when lock levels or speed values are modified.
2.5.9.8 - 2025-12-27
- Temporarily fix for alerts infos not showing issue.
2.5.9.7 - 2025-12-24
- Added custom preset system for saving and loading lock/speed configurations with custom names.
- Users can now save unlimited presets (e.g., "Custom 1", "Highway Settings", "City Streets").
- Load any saved preset with one click to quickly apply lock/speed settings.
- Delete unwanted presets with confirmation.
- Improved UI: Compact design with better spacing and dark mode support.
- Export/Import buttons reorganized into a cleaner layout.
- Preset items use semi-transparent backgrounds for better theme compatibility.
2.5.9.6 - 2025-08-05
- Bug fix: Enhanced "Copy Connected Segment Attribute" logic.
2.5.9.5 - 2025-07-31
- Minor bug fixes.
2.5.9.3 - 2025-07-04
- Updated logic for speed limit. Now it will not update speed limit set to 0 or -1.
2.5.9.2 - 2025-07-03-01
- improved logic for copy first connected segment name and/or city.
- Able to work with road type button in compact mode.
<b>2.5.9 - 2025-06-23</b><br>
- Added a confirmation popup before changing between Street and Footpath/Pedestrianised Area/Stairway types.<br>
- If you cancel, the segment will not be changed.<br>
- Prevents accidental deletion and recreation of special segment types.<br>
- Segment name copying now works for both one-way and two-way segments when converting to pedestrian types.<br>
- When "Copy connected Segment Name" is on, the script copies the name from the connected segment before conversion and applies it after.<br>
- Improved reliability for segment recreation and name copying.<br>
- Includes all previous improvements and bug fixes.<br>
2.5.8 - 2025-06-21</b><br>
- When "Set Street Name to None" is checked, the primary street is set to none and all alternate street names are removed.<br>
- When "Set city as none" is checked, all primary and alternate city names are set to none (empty city).<br>
- "Set street to none" now only handles the street name.<br>
- Added option for setting city to none.<br>
- Lock and speed settings can be exported or imported.<br>
- In case of shortcut key conflict, the shortcut key becomes null.<br>
- Default shortcut key is now "G".<br>
- Minor code cleanup.<br>
- Other behaviors remain unchanged.<br>
2.5.7.4 - 2025-06-15
- Set street to none only handles street name now.
- Added option for setting up city to none.
- The lock and speed setting can be exported or imported.
- Incase of the shortcutkey conflict, the shortcut key becomes null.
- Default shortcut key is now "G".
- Minor code cleanup.
2.5.7 - 2025-06-13
- Fixed: Autosave now works when "Copy Connected Segment Attribute" is enabled.
- When both "Autosave on Action" and "Copy Connected Segment Attribute" are checked, pressing the quick update button or shortcut will copy attributes and then autosave.
- All other options continue to function as before.
2.5.6 - 2025-06-08.02
- minor bugfixes and UI improvements.
2.5.5 - 2025-06-08.01
- Improved mutual exclusion logic for "Copy Connected Segment Attribute" and other checkboxes (except Autosave):
- Checking "Copy Connected Segment Attribute" now unchecks all other options (except Autosave), but does not disable them.
- Checking any other option (except Autosave) will uncheck "Copy Connected Segment Attribute".
- Added tooltips for all checkbox options.
- Fixed radio button selection logic for road types when using keyboard shortcuts.
- Minor bugfixes and UI improvements.
2.5.5 - 2025-06-08.01
- Minor bugfixes
2.5.4 - 2025-06-07.01
- Road Types can be selected using keyboard shortcuts.
- Fully migrated to WME SDK.
2.5.3 - 2025-05-31
- Improved shortcut keybind logic and UI.
- The unpaved attribute is now copied in Non-compact mode.
- Minor bug fixes.
2.5.2 - 2025-05-31
- Improved shortcut keybind logic and UI.
- Prevented double alerts and unnecessary alerts when keybind is empty.
- Minor bug fixes.
2.5.1 - 2025-05-30
- Added "Copy Connected Segment Attribute" option.
- When enabled, all other options (except Autosave) are automatically unchecked.
- Copies all major attributes from a connected segment: speed limit, primary name, alias name, city, paved/unpaved status, and lock level.
- Ensures mutually exclusive logic for this option in the UI.
2.5.0 - 2025-05-29
- Improved reliability of the unpaved toggle by adding a 500ms delay and fallback logic for both compact and non-compact UI modes.
- Now ensures paved/unpaved status is always set correctly, regardless of UI mode.
- Enhanced update logic: skips redundant updates for road type, lock, and speed if already set.
- Improved city name and segment name copying logic, with more robust alerts and feedback.
- UI improvements: disables lock/speed controls when their options are off; mutually exclusive logic for "Set Street To None" and "Copy connected Segment Name".
- Improved logging for easier debugging and tracking.
- Bug fixes and code cleanup for better reliability and maintainability.
2.4.9 - 2025-05-22
- Improved reliability of unpaved toggle (500ms delay).
- Minor bug fixes and code cleanup
2.4.8 - 2025-05-21
- Now pave/unpave works better. When unchecked, the segment will be paved!
2.4.7 - 2025-05-21
Added support for copying alternative segment name from connected segment
Thanks to -
- MapOmatic, without him this would not be possible.
2.4.6 - 2025-05-20
Added support for copying segment name from connected segment
Limitation -
- Currently Alt names wont be copied.
2.4.5 - 2025-05-11
Added Support for HRCS
Fixed -
- Various other bugs
*/
})();