英語で読めなかったのでAIに頼んで日本語に変更してもらいました
// ==UserScript==
// @name WME EZRoad Mod日本語版
// @namespace https://greasyfork.org/users/1087400
// @version 2.6.8.1
// @description Easily update roads
// @author https://greasyfork.org/en/users/1087400-kid4rm90s
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @exclude https://www.waze.com/user/*editor/*
// @exclude https://www.waze.com/*/user/*editor/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant unsafeWindow
// @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @license GNU GPL(v3)
// @connect greasyfork.org
// @require https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
// @require https://greasyfork.org/scripts/560385/code/WazeToastr.js
// @downloadURL https://update.greasyfork.org/scripts/528552/WME%20EZRoad%20Mod.user.js
// @updateURL https://update.greasyfork.org/scripts/528552/WME%20EZRoad%20Mod.meta.js
// ==/UserScript==
/*Script modified from WME EZRoad (https://greasyfork.org/en/scripts/518381-wme-ezsegments) original author: Michaelrosstarr and thanks to him*/
(function main() {
('use strict');
const updateMessage = `<strong>Version 2.6.8.1 - 2026-02-18:</strong><br>
- Fixed issue with copying the names from connected segment<br>
Now it will prioritise the first connected segment at Side A with a valid city in its address, and if none have a valid city, it will fallback to the first connected segment with any address<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
<br>`;
const scriptName = GM_info.script.name;
const scriptVersion = GM_info.script.version;
const downloadUrl = 'https://greasyfork.org/en/scripts/528552-wme-ezroad-mod/code/WME%20EZRoad%20Mod.user.js';
const forumURL = 'https://greasyfork.org/scripts/528552-wme-ezroad-mod/feedback';
let wmeSDK;
const roadTypes = [
{ id: 1, name: '高速道路', value: 3, shortcutKey: 'S+1' },
{ id: 2, name: 'ランプ', value: 4, shortcutKey: 'S+2' },
{ id: 3, name: '主要高速', value: 6, shortcutKey: 'S+3' },
{ id: 4, name: '一般高速', value: 7, shortcutKey: 'S+4' },
{ id: 5, name: 'プライマリー通り', value: 2, shortcutKey: 'S+5' },
{ id: 6, name: '通り', value: 1, shortcutKey: 'S+6' },
{ id: 7, name: '狭い通り', value: 22, shortcutKey: 'S+7' },
{ id: 8, name: 'オフロード', value: 8, shortcutKey: 'S+8' },
{ id: 9, name: '駐車場の道', value: 20, shortcutKey: 'S+9' },
{ id: 10, name: 'プライベート道', value: 17, shortcutKey: 'S+0' },
{ id: 11, name: 'フェリー', value: 15, shortcutKey: 'A+1' },
{ id: 12, name: '鉄道', value: 18, shortcutKey: 'A+2' },
{ id: 13, name: '滑走路', value: 19, shortcutKey: 'A+3' },
{ id: 14, name: '歩道', value: 5, shortcutKey: 'A+4' },
{ id: 15, name: '歩行者専用エリア', value: 10, shortcutKey: 'A+5' },
{ id: 16, name: '階段', 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('WME_EZRoads_Mod: ' + message);
} else {
console.log('WME_EZRoads_Mod: ', message);
}
};
unsafeWindow.SDK_INITIALIZED.then(initScript);
function initScript() {
wmeSDK = getWmeSdk({
scriptId: 'wme-ez-roads-mod',
scriptName: 'EZ Roads Mod',
});
WME_EZRoads_Mod_bootstrap();
}
const getCurrentCountry = () => {
return wmeSDK.DataModel.Countries.getTopCountry();
};
const getTopCity = () => {
return wmeSDK.DataModel.Cities.getTopCity();
};
const getAllCities = () => {
return wmeSDK.DataModel.Cities.getAll();
};
// --- NEW: Helper to get all connected segment IDs ---
function getConnectedSegmentIDs(segmentId) {
// Returns unique IDs of all segments connected to the given segment
const segs = [...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }), ...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true })];
const ids = segs.map((segment) => segment.id);
// Remove duplicates
return [...new Set(ids)];
}
// --- NEW: Helper to get the first connected segment's address (recursively) ---
function getFirstConnectedSegmentAddress(segmentId) {
const nonMatches = [];
const segmentIDsToSearch = [segmentId];
const hasValidCity = (id) => {
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;
}
const segPanel = openPanel;
if (!segPanel) {
log('Segment panel not available for flag attribute updates');
return;
}
// Flag attribute mappings: { flagName: { selectorType, selector, checkedValue } }
const flagMappings = {
unpaved: { selectorType: 'checkbox', name: 'unpaved' },
};
for (let flagName in flagMappings) {
const mapping = flagMappings[flagName];
const fromValue = fromSeg.flagAttributes[flagName] === true;
const toValue = toSeg.flagAttributes && toSeg.flagAttributes[flagName] === true;
// Only update if values differ
if (fromValue === toValue) {
continue;
}
try {
// Try to find and click the checkbox
let checkboxFound = false;
// Try method 1: wz-checkable-chip with icon
const iconClass = flagName === 'unpaved' ? '.w-icon-unpaved-fill' : `.w-icon-${flagName.toLowerCase()}-fill`;
const unpavedIcon = segPanel.querySelector(iconClass);
if (unpavedIcon) {
const chip = unpavedIcon.closest('wz-checkable-chip');
if (chip) {
chip.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via chip`);
continue;
}
}
// Try method 2: wz-checkbox with name attribute
const wzCheckbox = segPanel.querySelector(`wz-checkbox[name="${mapping.name}"]`);
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector(`input[type="checkbox"][name="${mapping.name}"]`);
if (hiddenInput && hiddenInput.checked !== fromValue) {
hiddenInput.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via wz-checkbox`);
continue;
}
}
// Try method 3: regular checkbox
const regularCheckbox = segPanel.querySelector(`input[type="checkbox"][name="${mapping.name}"]`);
if (regularCheckbox && regularCheckbox.checked !== fromValue) {
regularCheckbox.click();
checkboxFound = true;
log(`Updated flag attribute ${flagName} via regular checkbox`);
continue;
}
if (!checkboxFound) {
log(`Could not find UI element for flag attribute ${flagName}`);
}
} catch (e) {
log(`Error updating flag attribute ${flagName}: ${e}`);
}
}
}
// --- 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('EZRoads Mod', `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);
};
const WME_EZRoads_Mod_bootstrap = () => {
if (!document.getElementById('edit-panel') || !wmeSDK.DataModel.Countries.getTopCountry()) {
setTimeout(WME_EZRoads_Mod_bootstrap, 250);
return;
}
if (wmeSDK.State.isReady) {
WME_EZRoads_Mod_init();
} else {
wmeSDK.Events.once({ eventName: 'wme-ready' }).then(WME_EZRoads_Mod_init());
}
};
let openPanel;
const WME_EZRoads_Mod_init = () => {
log('Initing');
const options = getOptions();
const shortcutId = 'EZRoad_Mod_QuickUpdate';
// Only register if not already present
if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) {
registerShortcut(options.shortcutKey || 'g');
}
// --- ENHANCED: Add event listeners to each road-type chip for direct click handling ---
// Global flag to suppress attribute copy when chip is clicked
window.suppressCopySegmentAttributes = false;
function addRoadTypeChipListeners() {
const chipSelect = document.querySelector('.road-type-chip-select');
if (!chipSelect) return;
const chips = chipSelect.querySelectorAll('wz-checkable-chip');
chips.forEach((chip) => {
if (!chip._ezroadmod_listener) {
chip._ezroadmod_listener = true;
chip.addEventListener('click', function () {
// Log every chip click for debugging
log('Chip clicked: value=' + chip.getAttribute('value') + ', checked=' + chip.getAttribute('checked'));
setTimeout(() => {
// Only act if this chip is now the selected one (checked="")
if (chip.getAttribute('checked') === '') {
const rtValue = parseInt(chip.getAttribute('value'), 10);
log('Detected chip selection, applying EZRoadMod logic for roadType value: ' + rtValue);
if (isNaN(rtValue)) return;
const options = getOptions();
options.roadType = rtValue;
saveOptions(options);
if (typeof updateRoadTypeRadios === 'function') {
updateRoadTypeRadios(rtValue);
}
const selection = wmeSDK.Editing.getSelection();
if (selection && selection.objectType === 'segment') {
wmeSDK.Editing.setSelection({ selection });
}
setTimeout(() => {
log('Calling handleUpdate() after chip click for roadType value: ' + rtValue);
window.suppressCopySegmentAttributes = true;
Promise.resolve(handleUpdate()).finally(() => {
window.suppressCopySegmentAttributes = false;
});
}, 100);
}
}, 50);
});
}
});
}
// Call after panel is available and after any UI changes that might re-render the chips
setTimeout(addRoadTypeChipListeners, 1200);
// Also call after every edit panel mutation to re-attach listeners
// Observe the edit panel for segment changes and add the quick update button
const roadObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
for (let i = 0; i < mutation.addedNodes.length; i++) {
const addedNode = mutation.addedNodes[i];
if (addedNode.nodeType === Node.ELEMENT_NODE) {
let editSegment = addedNode.querySelector('#segment-edit-general');
if (editSegment) {
openPanel = editSegment;
const parentElement = editSegment.parentNode;
if (!parentElement.querySelector('[data-ez-roadmod-button="true"]')) {
log('Creating Quick Set Road button for this panel');
const quickButton = document.createElement('wz-button');
quickButton.setAttribute('type', 'button');
quickButton.setAttribute('style', 'margin-bottom: 5px; width: 100%');
quickButton.setAttribute('disabled', 'false');
quickButton.setAttribute('data-ez-roadmod-button', 'true');
quickButton.setAttribute('id', 'ez-roadmod-quick-button-' + Date.now()); // Unique ID using timestamp
quickButton.classList.add('send-button', 'ez-comment-button');
quickButton.textContent = 'Quick Update Segment';
parentElement.insertBefore(quickButton, editSegment);
quickButton.addEventListener('mousedown', () => handleUpdate());
log('Button created for current panel');
} else {
log('This panel already has the button, skipping creation');
}
// Always re-attach chip listeners after panel mutation
addRoadTypeChipListeners();
}
}
}
});
});
roadObserver.observe(document.getElementById('edit-panel'), {
childList: true,
subtree: true,
});
constructSettings();
function updateRoadTypeRadios(newValue) {
$(`input[name="defaultRoad"]`).each(function () {
if (parseInt($(this).attr('data-road-value'), 10) === newValue) {
$(this).prop('checked', true);
} else {
$(this).prop('checked', false);
}
});
}
// Register shortcut for each road type (move here, after handleUpdate is defined)
roadTypes.forEach((rt) => {
const shortcutId = `EZRoad_Mod_SelectRoadType_${rt.id}`;
// Prevent duplicate shortcut registration
if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) {
try {
wmeSDK.Shortcuts.createShortcut({
callback: () => {
const options = getOptions();
options.roadType = rt.value;
saveOptions(options);
updateRoadTypeRadios(rt.value);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success('EZRoads Mod', `Selected road type: <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('EZRoads Mod', '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(
'EZRoads Mod',
`Motorbike-only restriction applied to ${selection.ids.length} segment(s) ✓`,
false,
false,
3000
);
}
} else if (result === 'not_supported') {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.warning('EZRoads Mod', `Segment not found or is pedestrian type, cannot apply motorbike restriction`, false, false, 5000);
}
} else if (result === 'not_supported type') {
log(`Segment not supported type, cannot apply motorbike restriction`);
}
}).catch((error) => {
console.error('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(`[EZRoads Mod] Shortcut '${shortcutKey}' for Quick Update Segments enabled.`);
} catch (e) {
// If shortcut registration fails (e.g., conflict), register with no key so it appears in WME UI
console.warn('[EZRoads Mod] Shortcut registration failed:', e);
try {
wmeSDK.Shortcuts.createShortcut({
callback: handleUpdate,
description: 'Quick Update Segments.',
shortcutId,
shortcutKeys: null, // Register with no key so it appears in WME UI
});
console.log('[EZRoads Mod] Registered shortcut with no key due to conflict.');
} catch (e2) {
console.error('[EZRoads Mod] Failed to register shortcut with no key:', e2);
}
const options = getOptions();
options.shortcutKey = null;
saveOptions(options);
}
}
// ===== New Feature: One-click Geometry Fix =====
async function fixVisibleGeometryIssues() {
const options = getOptions();
if (!options.checkGeometryIssues) {
if (WazeToastr?.Alerts) WazeToastr.Alerts.info('EZRoad Mod', '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('EZRoad Mod', '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('EZRoad Mod', 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('EZRoad Mod', 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(`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(`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(`[EZRoad] 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(`[EZRoad] 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(`[EZRoad] Enabled turn ${turn.id} at node ${nodeId}`);
}
} catch (turnError) {
log(`[EZRoad] Could not enable turn ${turn.id}: ${turnError.message}`);
}
});
} catch (nodeError) {
log(`[EZRoad] Error processing turns at node ${nodeId}: ${nodeError.message}`);
}
});
log(`[EZRoad] Completed turn enablement for segment ${segmentId}`);
} catch (error) {
console.error(`[EZRoad] 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(`[EZRoad] 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(`[EZRoad] 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(`[EZRoad] Delete failed: ${ex.message}`);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error('EZRoad Mod', errorMsg);
} else {
alert(errorMsg);
}
return null;
}
// Create new segment
log(`[EZRoad] Creating new segment with road type ${targetRoadType}`);
const newSegmentId = wmeSDK.DataModel.Segments.addSegment({ geometry, roadType: targetRoadType });
if (!newSegmentId) {
log(`[EZRoad] Failed to create new segment`);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error('EZRoad Mod', '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(`[EZRoad] 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(`[EZRoad] 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(`[EZRoad] Enabling turns after conversion from pedestrian to routable type`);
enableAllTurnsForSegment(newSegmentId);
}, 300);
}
log(`[EZRoad] 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('EZRoad Mod', successMsg, false, false, 3000);
} else {
alert(`EZRoad Mod: ${successMsg}`);
}
return newSegmentId;
} catch (error) {
log(`[EZRoad] Error during segment recreation: ${error.message}`);
console.error('[EZRoad] Segment recreation error:', error);
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.error('EZRoad Mod', `Error recreating segment: ${error.message}`);
} else {
alert(`Error recreating segment: ${error.message}`);
}
return null;
}
};
// Show confirmation dialog
if (WazeToastr?.Alerts?.confirm) {
WazeToastr.Alerts.confirm(
'EZRoad Mod',
swapMsg,
performRecreation, // OK callback - perform the recreation
() => {
// User cancelled
log(`[EZRoad] Segment recreation cancelled by user`);
},
'Continue',
'Cancel'
);
return undefined; // Return undefined to indicate async dialog is pending
} else if (!window.confirm(swapMsg)) {
log(`[EZRoad] 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(`Error validating segments: ${e}`);
return;
}
log('Updating RoadType');
const options = getOptions();
let alertMessageParts = [];
let updatedRoadType = false;
let updatedLockLevel = false;
let updatedSpeedLimit = false;
let updatedPaved = false;
let updatedCityName = false;
let updatedSegmentName = false;
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('updateSegment error (will retry with individual properties): ' + updateError);
// Fallback: try updating properties individually
const propsToTry = [
{ name: 'fwdSpeedLimit', value: connectedSeg.fwdSpeedLimit },
{ name: 'revSpeedLimit', value: connectedSeg.revSpeedLimit },
{ name: 'roadType', value: connectedSeg.roadType },
{ name: 'lockRank', value: connectedSeg.lockRank },
{ name: 'elevationLevel', value: connectedSeg.elevationLevel },
{ name: 'direction', value: getDirectionFromSegment(connectedSeg) },
];
for (let prop of propsToTry) {
try {
if (prop.value !== undefined && prop.value !== null) {
const updateObj = { segmentId: id };
updateObj[prop.name] = prop.value;
wmeSDK.DataModel.Segments.updateSegment(updateObj);
}
} catch (e) {
log(`Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log('updateAddress error: ' + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
alertMessageParts.push(`Copied all attributes from connected segment.`);
found = true;
break;
}
}
// If no connected segment with valid street name was found, fallback to any connected segment (like the other logic)
if (!found) {
let fallbackSegId = null;
const segObj = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const fromNode = segObj.fromNodeId;
const toNode = segObj.toNodeId;
const allSegs = wmeSDK.DataModel.Segments.getAll();
for (let s of allSegs) {
if (s.id !== id && (s.fromNodeId === fromNode || s.toNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === toNode)) {
fallbackSegId = s.id;
break;
}
}
if (fallbackSegId) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: fallbackSegId });
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: connectedSeg.fwdSpeedLimit,
revSpeedLimit: connectedSeg.revSpeedLimit,
roadType: connectedSeg.roadType,
lockRank: connectedSeg.lockRank,
elevationLevel: connectedSeg.elevationLevel,
direction: getDirectionFromSegment(connectedSeg),
});
} catch (updateError) {
log('updateSegment error in fallback (will retry with individual properties): ' + updateError);
// Fallback: try updating properties individually
const propsToTry = [
{ name: 'fwdSpeedLimit', value: connectedSeg.fwdSpeedLimit },
{ name: 'revSpeedLimit', value: connectedSeg.revSpeedLimit },
{ name: 'roadType', value: connectedSeg.roadType },
{ name: 'lockRank', value: connectedSeg.lockRank },
{ name: 'elevationLevel', value: connectedSeg.elevationLevel },
{ name: 'direction', value: getDirectionFromSegment(connectedSeg) },
];
for (let prop of propsToTry) {
try {
if (prop.value !== undefined && prop.value !== null) {
const updateObj = { segmentId: id };
updateObj[prop.name] = prop.value;
wmeSDK.DataModel.Segments.updateSegment(updateObj);
}
} catch (e) {
log(`Failed to update ${prop.name}: ` + e);
}
}
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: connectedSeg.primaryStreetId,
alternateStreetIds: connectedSeg.alternateStreetIds || [],
});
} catch (addrError) {
log('updateAddress error in fallback: ' + addrError);
}
// Copy all flag attributes
copyFlagAttributes(connectedSeg.id, id);
alertMessageParts.push(`Copied all attributes from connected segment.`);
log(`Copied all attributes from connected segment (fallback, no valid street name).`);
} else {
alertMessageParts.push(`No connected segment found to copy attributes.`);
}
}
} catch (error) {
console.error('Error copying all attributes:', error);
}
}, 100)
);
});
Promise.all(updatePromises).then(() => {
if (alertMessageParts.length) {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.info('EZRoads Mod', alertMessageParts.join('<br>'), false, false, 5000);
} else {
alert('EZRoads Mod: ' + alertMessageParts.join('\n'));
}
}
// --- AUTOSAVE LOGIC HERE ---
if (options.autosave) {
setTimeout(() => {
log('Delayed Autosave starting...');
wmeSDK.Editing.save().then(() => {
log('Delayed Autosave completed.');
});
}, 600);
}
});
return;
}
// Apply motorbike restriction ONCE for all selected segments (before individual updates)
let motorcycleRestrictionApplied = false;
if (options.restrictExceptMotorbike) {
log('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(
'EZRoads Mod',
`Motorbike-only restriction applied to ${selection.ids.length} segment(s) ✓`,
false,
false,
3000
);
}
} else if (result === 'not_supported') {
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.warning(
'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('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(`Segment ID: ${id}, Current Road Type: ${seg.roadType}, Target Road Type: ${options.roadType}, Target Road Name : ${selectedRoad.name}`); // Log current and target road type
if (seg.roadType === options.roadType) {
log(`Segment ID: ${id} already has the target road type: ${options.roadType}. Skipping update.`);
alertMessageParts.push(`Road Type: <b>${selectedRoad.name} exists. Skipping update.</b>`);
updatedRoadType = true;
} else {
try {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
roadType: options.roadType,
});
log('Road type updated successfully.');
alertMessageParts.push(`Road Type: <b>${selectedRoad.name}</b>`);
updatedRoadType = true;
} catch (error) {
console.error('Error updating road type:', error);
}
}
}
}, 200)
); // 200ms delay before road type update
// Set lock if enabled
updatePromises.push(
delayedUpdate(() => {
if (options.setLock) {
const rank = wmeSDK.State.getUserInfo().rank;
const selectedRoad = roadTypes.find((rt) => rt.value === options.roadType);
if (selectedRoad) {
let lockSetting = options.locks.find((l) => l.id === selectedRoad.id);
if (lockSetting) {
let toLock = lockSetting.lock;
if (toLock === 'HRCS') {
toLock = getHighestSegLock(id);
} else {
toLock = parseInt(toLock, 10);
toLock = Math.max(toLock - 1, 0); // Adjust to 0-based rank, ensuring it does not go below 0
}
if (rank < toLock) toLock = rank;
log(toLock);
try {
const seg = wmeSDK.DataModel.Segments.getById({
segmentId: id,
});
let displayLockLevel = toLock === 'HRCS' || isNaN(toLock) ? 'HRCS' : `L${toLock + 1}`;
let currentDisplayLockLevel;
if (seg.lockRank === 'HRCS') {
// Should not happen, but for safety
currentDisplayLockLevel = 'HRCS';
} else {
currentDisplayLockLevel = `L${seg.lockRank + 1}`;
}
if (seg.lockRank === toLock || (lockSetting.lock === 'HRCS' && currentDisplayLockLevel === displayLockLevel)) {
// Compare lock levels
log(`Segment ID: ${id} already has the target lock level: ${displayLockLevel}. Skipping update.`);
alertMessageParts.push(`Lock Level: <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('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}`);
}
// Updated unpaved handler with SegmentFlagAttributes and fallback
updatePromises.push(
delayedUpdate(() => {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const isPedestrian = isPedestrianType(seg.roadType);
if (isPedestrian) {
// Always set as paved for pedestrian types, regardless of checkbox
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let pavedToggled = false;
if (isUnpaved) {
// Click to set as paved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to paved for pedestrian type)');
pavedToggled = true;
}
}
if (!pavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to paved, non-compact mode, pedestrian type)');
pavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode paved toggle method failed: ' + e);
}
}
if (pavedToggled) {
alertMessageParts.push(`Paved Status: <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
// 3a. Copy segment name from connected segment if enabled
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 (ALL A side segments), then B side (only if no A side)
let segsToTry = [];
if (aSideSegs.length > 0) {
// Prefer A side - add all of them
segsToTry = aSideSegs.map((s) => s.id);
} else if (bSideSegs.length > 0) {
// Only use B side if NO A side segments exist
segsToTry = bSideSegs.map((s) => s.id);
}
let found = false;
for (let connectedSegId of segsToTry) {
const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId });
if (!connectedSeg) continue;
const streetId = connectedSeg.primaryStreetId;
const altStreetIds = connectedSeg.alternateStreetIds || [];
let street = null;
try {
street = wmeSDK.DataModel.Streets.getById({ streetId });
// Ensure street is fully loaded
if (street && street.name === undefined && street.cityId === undefined) {
log(`Street ${streetId} not fully loaded, skipping`);
continue;
}
} catch (e) {
log(`Error getting street ${streetId}: ${e}`);
continue;
}
// Get alternate street names
let altNames = [];
altStreetIds.forEach((altId) => {
try {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) altNames.push(altStreet.name);
} catch (e) {
log(`Error getting alternate street ${altId}: ${e}`);
}
});
// If any connected segment has a name or alias, use it
if (street && (street.name || street.englishName || street.signText || altNames.length > 0)) {
if (options.setStreetCity && street) {
const emptyCity = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' });
// Try to find or create a street with the same name but in the empty city
let noneStreet = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: street.name || '',
});
if (!noneStreet) {
noneStreet = wmeSDK.DataModel.Streets.addStreet({
streetName: street.name || '',
cityId: emptyCity.id,
});
}
// For alternate streets, also convert them to the empty city
let newAltStreetIds = [];
altStreetIds.forEach((altId) => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId });
if (altStreet && altStreet.name) {
let altInEmptyCity = wmeSDK.DataModel.Streets.getStreet({
cityId: emptyCity.id,
streetName: altStreet.name || '',
});
if (!altInEmptyCity) {
altInEmptyCity = wmeSDK.DataModel.Streets.addStreet({
streetName: altStreet.name || '',
cityId: emptyCity.id,
});
}
newAltStreetIds.push(altInEmptyCity.id);
}
});
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: noneStreet.id,
alternateStreetIds: newAltStreetIds,
});
let aliasMsg = altNames.length ? ` (Alternatives: ${altNames.join(', ')})` : '';
alertMessageParts.push(`Copied Name: <b>${street.name || ''}</b>${aliasMsg}`);
updatedSegmentName = true;
pushCityNameAlert(emptyCity.id, alertMessageParts);
updatedCityName = true;
} else {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: streetId,
alternateStreetIds: altStreetIds,
});
let aliasMsg = altNames.length ? ` (Alternatives: ${altNames.join(', ')})` : '';
alertMessageParts.push(`Copied Name: <b>${street.name || ''}</b>${aliasMsg}`);
updatedSegmentName = true;
pushCityNameAlert(street.cityId, alertMessageParts);
updatedCityName = true;
}
found = true;
break;
}
}
if (!found) {
alertMessageParts.push(`Copied Name: <b>None (no connected segment found)</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('EZRoads Mod', `Segment updated with: ${message}`, false, false, 3000);
} else {
alert('EZRoads Mod: Segment updated (WazeToastr Alerts not available)');
}
}
};
// Autosave - DELAYED AUTOSAVE
if (options.autosave) {
setTimeout(() => {
log('Delayed Autosave starting...');
wmeSDK.Editing.save().then(() => {
log('Delayed Autosave completed.');
showAlert();
});
}, 600); // 1000ms (1 second) delay before autosave
} else {
showAlert();
}
});
};
const constructSettings = () => {
const localOptions = getOptions();
let currentRoadType = localOptions.roadType;
const update = (key, value) => {
const options = getOptions();
options[key] = value;
localOptions[key] = value;
saveOptions(options);
};
// Update lock level for a specific road type
const updateLockLevel = (roadTypeId, lockLevel) => {
const options = getOptions();
const lockIndex = options.locks.findIndex((l) => l.id === roadTypeId);
if (lockIndex !== -1) {
options.locks[lockIndex].lock = lockLevel; // Keep as string to handle 'HRCS'
localOptions.locks = options.locks;
saveOptions(options);
// Trigger preset list refresh if settings panel is open
if ($('#ezroadsmod-presets-list').length) {
$('#ezroadsmod-presets-list').trigger('refresh-presets');
}
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success('EZRoads Mod', 'Lock Levels saved!', false, false, 1500);
} else {
alert('EZRoads Mod: Lock Levels saved!');
}
}
};
// Update speed for a specific road type
const updateSpeed = (roadTypeId, speed) => {
const options = getOptions();
const speedIndex = options.speeds.findIndex((s) => s.id === roadTypeId);
let speedValue = parseInt(speed, 10);
if (isNaN(speedValue)) {
speedValue = -1;
}
log(`Updating speed for road type ${roadTypeId} to ${speedValue}`);
if (speedIndex !== -1) {
options.speeds[speedIndex].speed = speedValue;
localOptions.speeds = options.speeds;
saveOptions(options);
// Trigger preset list refresh if settings panel is open
if ($('#ezroadsmod-presets-list').length) {
$('#ezroadsmod-presets-list').trigger('refresh-presets');
}
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success('EZRoads Mod', 'Speed Values saved!', false, false, 1500);
} else {
alert('EZRoads Mod: Speed Values saved!');
}
}
};
// Reset all options to defaults
const resetOptions = () => {
saveOptions(defaultOptions);
// Refresh the page to reload settings
window.location.reload();
};
// Checkbox option definitions
const checkboxOptions = [
{
id: 'setStreet',
text: '通り名をなしに設定',
key: 'setStreet',
tooltip: '選択したセグメントの通り名をなしに設定します。チェックを外すと、通り名は変わりません。',
},
{
id: 'setStreetCity',
text: '都市をなしに設定(チェック解除で自動追加)',
key: 'setStreetCity',
tooltip: 'チェックすると、選択したセグメントの主要道路と代替道路の両方の都市が「なし」に設定されます。チェック解除すると、利用可能な都市名が主要道路と代替道路の両方に自動的に追加されます。',
},
{
id: 'autosave',
text: 'アクション時に自動保存',
key: 'autosave',
tooltip: 'セグメントの更新後に自動的に保存します。',
},
{
id: 'unpaved',
text: '舗装されていないに設定(チェック解除で舗装)',
key: 'unpaved',
tooltip: 'セグメントを未舗装に設定します。チェック解除して舗装に設定します。',
},
{
id: 'setLock',
text: 'ロックレベルを設定',
key: 'setLock',
tooltip: '選択した道路タイプのロックレベルを設定します。ロックレベルドロップダウンも有効にします。',
},
{
id: 'updateSpeed',
text: '速度制限を更新',
key: 'updateSpeed',
tooltip: '選択した道路タイプの速度制限を更新します。速度入力フィールドも有効にします。',
},
{
id: 'enableUTurn',
text: 'Uターンを有効にする',
key: 'enableUTurn',
tooltip: 'クイック更新がトリガーされたときに、選択したセグメントのUターンを有効にします。片方向と両方向の両方のセグメントで機能し、両端にUターンを追加します。',
},
{
id: 'copySegmentName',
text: '接続されたセグメント名をコピー',
key: 'copySegmentName',
tooltip: "接続されたセグメントから名前と都市を選択したセグメントにコピーします。「都市をなしに設定」が有効な場合、コピーされた値に関わらず都市が「なし」に設定されます。",
},
{
id: 'copySegmentAttributes',
text: '接続されたセグメント属性をコピー',
key: 'copySegmentAttributes',
tooltip:
'接続されたセグメントから、道路タイプ、ロックレベル、速度制限、舗装/未舗装ステータス、主要道路と代替道路の名前、および都市など、すべての主要な属性をコピーします。有効にすると、自動保存以外のすべてのオプションをオーバーライドします。ショートカットキーまたは(クイック更新セグメント)を使用して適用します。',
},
{
id: 'showSegmentLength',
text: 'セグメント長 <=20mを表示',
key: 'showSegmentLength',
tooltip: '表示されるマップエリア内の20メートル以下のセグメントについて、オレンジ色の円に白いフォントでセグメント長を表示します。',
},
{
id: 'checkGeometryIssues',
text: 'ノード近くのジオメトリ問題を確認',
key: 'checkGeometryIssues',
tooltip: '中間のジオメトリノードが開始ノードまたは終了ノードに近すぎるかどうかをチェックします。下の閾値距離を設定します。問題が見つかった場合はピンアイコンを表示します。',
},
{
id: 'restrictExceptMotorbike',
text: 'バイク以外を制限(自動)',
key: 'restrictExceptMotorbike',
tooltip: 'UI自動化を使用してバイク専用車両制限を自動的に追加します。全日、両方向の全セグメントに適用されます。バイク以外のすべての車両をブロックします。ショートカットキー(Alt+R)またはクイック更新セグメントを使用して適用できます。',
},
];
// 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' : ''} title="ロックレベル">
${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' : ''} title="速度制限(km/h)">
</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 h2, #ezroadsmod-settings h5 {
margin-top: 0;
margin-bottom: 10px;
}
.ezroadsmod-section {
margin-bottom: 15px;
overflow: visible;
}
.ezroadsmod-option {
margin-bottom: 10px;
overflow: visible;
}
.ezroadsmod-radio-container {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: visible;
}
.ezroadsmod-radio-container input[type="radio"] {
margin-right: 0px;
flex-shrink: 0;
}
.ezroadsmod-radio-container label {
min-width: 100px;
max-width: 100px;
margin-right: 4px;
text-align: left;
flex-shrink: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ezroadsmod-radio-container select {
width: 75px;
min-width: 75px;
padding: 4px 18px 4px 4px;
margin-right: 3px;
flex-shrink: 0;
background-color: #ffffff;
border: 1px solid #ccc;
border-radius: 3px;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1px center;
background-size: 12px;
font-size: 12px;
overflow: visible;
}
.ezroadsmod-radio-container input.road-speed {
width: 50px;
min-width: 50px;
padding: 4px 4px;
flex-shrink: 0;
border: 1px solid #ccc;
border-radius: 3px;
background-color: #ffffff;
font-size: 12px;
}
.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>現在のバージョン: <b>${scriptVersion}</b></div>
<div>キーバインディングを更新: <kbd>G</kbd></div>
<div style="font-size: 0.8em;">WMEのキーボード設定で変更できます!</div>
</div>`);
scriptContentPane.append(header);
// Road type and options header
const roadTypeHeader = $(`<div class="ezroads-section" style="margin-bottom: 5px;">
<div style="display: flex; align-items: flex-start; gap: 6px; padding: 0 8px 0 27px; margin-bottom: 8px;">
<div style="min-width: 100px; text-align: left; font-weight: bold; flex-shrink: 0;">道路タイプ</div>
<div style="width: 75px; text-align: center; font-weight: bold; flex-shrink: 0;">ロック</div>
<div style="width: 50px; text-align: center; font-weight: bold; flex-shrink: 0;">速度</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>追加オプション</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;">閾値距離(メートル):</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="セグメントのエンドポイント近くのジオメトリノードをチェックするためのメートル単位の距離">
</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">すべてのオプションをリセット</button>`);
resetButton.on('click', function () {
if (confirm('すべてのオプションをデフォルト値にリセットしてもよろしいですか?ウェブページが再読み込みされます!')) {
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;">ロック/速度設定をエクスポート</button>
<button id="ezroadsmod-import-btn" style="font-size:0.9em;padding:4px 8px;white-space:nowrap;">ロック/速度設定をインポート</button>
</div>
<input id="ezroadsmod-import-input" type="text" placeholder="ここに設定を貼り付けてください" 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('EZRoads Mod', 'ロック/速度設定とすべてのプリセットがクリップボードにコピーされました!', false, false, 2000);
} else {
alert('ロック/速度設定とすべてのプリセットがクリップボードにコピーされました!');
}
},
() => {
alert('設定をクリップボードにコピーできませんでした。');
}
);
});
// Import logic
$(document).on('click', '#ezroadsmod-import-btn', function () {
const importStr = $('#ezroadsmod-import-input').val();
if (!importStr) {
alert('インポートする設定文字列を貼り付けてください。');
return;
}
let importData;
try {
importData = JSON.parse(importStr);
} catch (e) {
alert('無効な設定文字列です!');
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('EZRoads Mod', message, false, false, 2000);
} else {
alert('Config imported and applied!');
}
} else {
alert('Config missing lock/speed data!');
}
});
// --- カスタムプリセット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;">カスタムロック/速度プリセット</div>
<div style="margin-bottom:6px;">
<input id="ezroadsmod-preset-name" type="text" placeholder="プリセット名" 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;">保存</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;">保存されたプリセットはありません。</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;">現在</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:15px;">
<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;">読込</button>
<button class="ezroadsmod-delete-preset-btn" data-preset-name="${presetName}" style="background-color:#f44336; color:white;font-size:0.9em;padding:3px 8px;">削除</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('プリセット名を入力してください。');
return;
}
const presets = getCustomPresets();
if (presets[presetName]) {
if (!confirm(`プリセット「${presetName}」は既に存在します。上書きしますか?`)) {
return;
}
}
if (saveCustomPreset(presetName)) {
$('#ezroadsmod-preset-name').val('');
refreshPresetsList();
if (WazeToastr?.Alerts) {
WazeToastr.Alerts.success('EZRoads Mod', `プリセット「${presetName}」が保存されました!`, false, false, 2000);
} else {
alert(`プリセット「${presetName}」が保存されました!`);
}
}
});
// 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('EZRoads Mod', `プリセット「${presetName}」が読み込まれました!`, false, false, 2000);
} else {
alert(`プリセット「${presetName}」が読み込まれました!`);
}
} else {
alert(`プリセット「${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('EZRoads Mod', `Preset "${presetName}" deleted!`, false, false, 2000);
} else {
alert(`Preset "${presetName}" deleted!`);
}
} else {
alert(`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.');
}
}
})();