您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily update roads
// ==UserScript== // @name WME EZRoad Mod // @namespace https://gf.qytechs.cn/users/1087400 // @version 2.5.9.4 // @description Easily update roads // @author https://gf.qytechs.cn/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 gf.qytechs.cn // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js // @require https://update.gf.qytechs.cn/scripts/509664/WME%20Utils%20-%20Bootstrap.js // ==/UserScript== /*Script modified from WME EZRoad (https://gf.qytechs.cn/en/scripts/518381-wme-ezsegments) original author: Michaelrosstarr and thanks to him*/ (function main() { 'use strict'; const updateMessage = ` <b>2.5.9.4 - 2025-07-15</b><br> - Bug fix: Enhanced "Copy Connected Segment Attribute" logic.<br>`; const scriptName = GM_info.script.name; const scriptVersion = GM_info.script.version; const downloadUrl = 'https://gf.qytechs.cn/scripts/528552-wme-ezroad-mod/code/wme-ezroad-mod.user.js'; const forumURL = 'https://gf.qytechs.cn/scripts/528552-wme-ezroad-mod/feedback'; let wmeSDK; const roadTypes = [ { id: 1, name: 'Motorway', value: 3, shortcutKey: 'S+1' }, { id: 2, name: 'Ramp', value: 4, shortcutKey: 'S+2' }, { id: 3, name: 'Major Highway', value: 6, shortcutKey: 'S+3' }, { id: 4, name: 'Minor Highway', value: 7, shortcutKey: 'S+4' }, { id: 5, name: 'Primary Street', value: 2, shortcutKey: 'S+5' }, { id: 6, name: 'Street', value: 1, shortcutKey: 'S+6' }, { id: 7, name: 'Narrow Street', value: 22, shortcutKey: 'S+7' }, { id: 8, name: 'Offroad', value: 8, shortcutKey: 'S+8' }, { id: 9, name: 'Parking Road', value: 20, shortcutKey: 'S+9' }, { id: 10, name: 'Private Road', value: 17, shortcutKey: 'S+0' }, { id: 11, name: 'Ferry', value: 15, shortcutKey: 'A+1' }, { id: 12, name: 'Railway', value: 18, shortcutKey: 'A+2' }, { id: 13, name: 'Runway', value: 19, shortcutKey: 'A+3' }, { id: 14, name: 'Foothpath', value: 5, shortcutKey: 'A+4' }, { id: 15, name: 'Pedestrianised Area', value: 10, shortcutKey: 'A+5' }, { id: 16, name: 'Stairway', value: 16, shortcutKey: 'A+6' }, ]; const defaultOptions = { roadType: 1, unpaved: false, setStreet: false, setStreetCity: false, setStreetState: false, autosave: false, setSpeed: 40, setLock: false, updateSpeed: false, copySegmentName: false, locks: roadTypes.map((roadType) => ({ id: roadType.id, lock: String(1) })), speeds: roadTypes.map((roadType) => ({ id: roadType.id, speed: 40 })), copySegmentAttributes: false, shortcutKey: 'g', }; const locks = [ { id: 1, value: '1' }, { id: 2, value: '2' }, { id: 3, value: '3' }, { id: 4, value: '4' }, { id: 5, value: '5' }, { id: 6, value: '6' }, { id: 'HRCS', value: 'HRCS' }, ]; const log = (message) => { if (typeof message === 'string') { console.log('WME_EZRoads_Mod: ' + message); } else { console.log('WME_EZRoads_Mod: ', message); } }; unsafeWindow.SDK_INITIALIZED.then(initScript); function initScript() { wmeSDK = getWmeSdk({ scriptId: 'wme-ez-roads-mod', scriptName: 'EZ Roads Mod', }); WME_EZRoads_Mod_bootstrap(); } const getCurrentCountry = () => { return wmeSDK.DataModel.Countries.getTopCountry(); }; const getTopCity = () => { return wmeSDK.DataModel.Cities.getTopCity(); }; const getAllCities = () => { return wmeSDK.DataModel.Cities.getAll(); }; // --- NEW: Helper to get all connected segment IDs --- function getConnectedSegmentIDs(segmentId) { // Returns unique IDs of all segments connected to the given segment const segs = [...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }), ...wmeSDK.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true })]; const ids = segs.map((segment) => segment.id); // Remove duplicates return [...new Set(ids)]; } // --- NEW: Helper to get the first connected segment's address (recursively) --- function getFirstConnectedSegmentAddress(segmentId) { const nonMatches = []; const segmentIDsToSearch = [segmentId]; const hasAddress = (id) => { const addr = wmeSDK.DataModel.Segments.getAddress({ segmentId: id }); return addr && !addr.isEmpty; }; while (segmentIDsToSearch.length > 0) { const startSegmentID = segmentIDsToSearch.pop(); const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID); const hasAddrSegmentId = connectedSegmentIDs.find(hasAddress); if (hasAddrSegmentId) { return wmeSDK.DataModel.Segments.getAddress({ segmentId: hasAddrSegmentId }); } nonMatches.push(startSegmentID); connectedSegmentIDs.forEach((segmentID) => { if (!nonMatches.includes(segmentID) && !segmentIDsToSearch.includes(segmentID)) { segmentIDsToSearch.push(segmentID); } }); } return null; } const saveOptions = (options) => { window.localStorage.setItem('WME_EZRoads_Mod_Options', JSON.stringify(options)); }; const getOptions = () => { const savedOptions = JSON.parse(window.localStorage.getItem('WME_EZRoads_Mod_Options')) || {}; // Deep merge for locks and speeds arrays const mergeById = (defaults, saved, key) => { if (!Array.isArray(defaults)) return defaults; if (!Array.isArray(saved)) return defaults; return defaults.map((def) => { const found = saved.find((s) => s.id === def.id); return found ? { ...def, ...found } : def; }); }; const mergedLocks = mergeById( defaultOptions.locks, (savedOptions.locks || []).map((l) => ({ ...l, lock: String(l.lock) })), 'locks' ); const mergedSpeeds = mergeById(defaultOptions.speeds, savedOptions.speeds || [], 'speeds'); return { ...defaultOptions, ...savedOptions, locks: mergedLocks, speeds: mergedSpeeds, }; }; const WME_EZRoads_Mod_bootstrap = () => { if (!document.getElementById('edit-panel') || !wmeSDK.DataModel.Countries.getTopCountry()) { setTimeout(WME_EZRoads_Mod_bootstrap, 250); return; } if (wmeSDK.State.isReady) { WME_EZRoads_Mod_init(); } else { wmeSDK.Events.once({ eventName: 'wme-ready' }).then(WME_EZRoads_Mod_init()); } }; let openPanel; const WME_EZRoads_Mod_init = () => { log('Initing'); const options = getOptions(); const shortcutId = 'EZRoad_Mod_QuickUpdate'; // Only register if not already present if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) { registerShortcut(options.shortcutKey || 'g'); } // --- ENHANCED: Add event listeners to each road-type chip for direct click handling --- // Global flag to suppress attribute copy when chip is clicked window.suppressCopySegmentAttributes = false; function addRoadTypeChipListeners() { const chipSelect = document.querySelector('.road-type-chip-select'); if (!chipSelect) return; const chips = chipSelect.querySelectorAll('wz-checkable-chip'); chips.forEach((chip) => { if (!chip._ezroadmod_listener) { chip._ezroadmod_listener = true; chip.addEventListener('click', function () { // Log every chip click for debugging log('Chip clicked: value=' + chip.getAttribute('value') + ', checked=' + chip.getAttribute('checked')); setTimeout(() => { // Only act if this chip is now the selected one (checked="") if (chip.getAttribute('checked') === '') { const rtValue = parseInt(chip.getAttribute('value'), 10); log('Detected chip selection, applying EZRoadMod logic for roadType value: ' + rtValue); if (isNaN(rtValue)) return; const options = getOptions(); options.roadType = rtValue; saveOptions(options); if (typeof updateRoadTypeRadios === 'function') { updateRoadTypeRadios(rtValue); } const selection = wmeSDK.Editing.getSelection(); if (selection && selection.objectType === 'segment') { wmeSDK.Editing.setSelection({ selection }); } setTimeout(() => { log('Calling handleUpdate() after chip click for roadType value: ' + rtValue); window.suppressCopySegmentAttributes = true; Promise.resolve(handleUpdate()).finally(() => { window.suppressCopySegmentAttributes = false; }); }, 100); } }, 50); }); } }); } // Call after panel is available and after any UI changes that might re-render the chips setTimeout(addRoadTypeChipListeners, 1200); // Also call after every edit panel mutation to re-attach listeners // Observe the edit panel for segment changes and add the quick update button const roadObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { for (let i = 0; i < mutation.addedNodes.length; i++) { const addedNode = mutation.addedNodes[i]; if (addedNode.nodeType === Node.ELEMENT_NODE) { let editSegment = addedNode.querySelector('#segment-edit-general'); if (editSegment) { openPanel = editSegment; const parentElement = editSegment.parentNode; if (!parentElement.querySelector('[data-ez-roadmod-button="true"]')) { log('Creating Quick Set Road button for this panel'); const quickButton = document.createElement('wz-button'); quickButton.setAttribute('type', 'button'); quickButton.setAttribute('style', 'margin-bottom: 5px; width: 100%'); quickButton.setAttribute('disabled', 'false'); quickButton.setAttribute('data-ez-roadmod-button', 'true'); quickButton.setAttribute('id', 'ez-roadmod-quick-button-' + Date.now()); // Unique ID using timestamp quickButton.classList.add('send-button', 'ez-comment-button'); quickButton.textContent = 'Quick Update Segment'; parentElement.insertBefore(quickButton, editSegment); quickButton.addEventListener('mousedown', () => handleUpdate()); log('Button created for current panel'); } else { log('This panel already has the button, skipping creation'); } // Always re-attach chip listeners after panel mutation addRoadTypeChipListeners(); } } } }); }); roadObserver.observe(document.getElementById('edit-panel'), { childList: true, subtree: true, }); constructSettings(); function updateRoadTypeRadios(newValue) { $(`input[name="defaultRoad"]`).each(function () { if (parseInt($(this).attr('data-road-value'), 10) === newValue) { $(this).prop('checked', true); } else { $(this).prop('checked', false); } }); } // Register shortcut for each road type (move here, after handleUpdate is defined) roadTypes.forEach((rt) => { const shortcutId = `EZRoad_Mod_SelectRoadType_${rt.id}`; // Prevent duplicate shortcut registration if (!wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) { try { wmeSDK.Shortcuts.createShortcut({ callback: () => { const options = getOptions(); options.roadType = rt.value; saveOptions(options); updateRoadTypeRadios(rt.value); if (WazeWrap?.Alerts) { WazeWrap.Alerts.success('EZRoads Mod', `Selected road type: <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}`); } } }); log('Completed Init'); }; // Helper to register the shortcut, avoids duplicate code function registerShortcut(shortcutKey) { if (!wmeSDK?.Shortcuts) return; const shortcutId = 'EZRoad_Mod_QuickUpdate'; // Always delete before creating to avoid duplicates if (wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId })) { wmeSDK.Shortcuts.deleteShortcut({ shortcutId }); } try { wmeSDK.Shortcuts.createShortcut({ callback: handleUpdate, description: 'Quick Update Segments.', shortcutId, shortcutKeys: shortcutKey, }); console.log(`[EZRoads Mod] Shortcut '${shortcutKey}' for Quick Update Segments enabled.`); } catch (e) { // If shortcut registration fails (e.g., conflict), register with no key so it appears in WME UI console.warn('[EZRoads Mod] Shortcut registration failed:', e); try { wmeSDK.Shortcuts.createShortcut({ callback: handleUpdate, description: 'Quick Update Segments.', shortcutId, shortcutKeys: null, // Register with no key so it appears in WME UI }); console.log('[EZRoads Mod] Registered shortcut with no key due to conflict.'); } catch (e2) { console.error('[EZRoads Mod] Failed to register shortcut with no key:', e2); } const options = getOptions(); options.shortcutKey = null; saveOptions(options); } } const getEmptyCity = () => { return ( wmeSDK.DataModel.Cities.getCity({ cityName: '', countryId: getCurrentCountry().id, }) || wmeSDK.DataModel.Cities.addCity({ cityName: '', countryId: getCurrentCountry().id, }) ); }; const delayedUpdate = (updateFn, delay) => { return new Promise((resolve) => { setTimeout(() => { updateFn(); resolve(); }, delay); }); }; function getHighestSegLock(segID) { const segObj = wmeSDK.DataModel.Segments.getById({ segmentId: segID }); if (!segObj) { console.warn(`Segment object with ID ${segID} not found in DataModel.Segments.`); return 1; // Default lock level if segment not found } const segType = segObj.roadType; const checkedSegs = []; let forwardLock = null; let reverseLock = null; function processForNode(forwardID) { checkedSegs.push(forwardID); const seg = wmeSDK.DataModel.Segments.getById({ segmentId: forwardID }); if (!seg) return forwardLock; const forNodeId = seg.toNodeId; if (!forNodeId) return forwardLock; // Get all segments connected to this node const allSegs = wmeSDK.DataModel.Segments.getAll(); const forNodeSegs = allSegs.filter((s) => s.fromNodeId === forNodeId || s.toNodeId === forNodeId).map((s) => s.id); // Remove the current segment from the list const filteredSegs = forNodeSegs.filter((id) => id !== forwardID); for (let i = 0; i < filteredSegs.length; i++) { const conSegObj = wmeSDK.DataModel.Segments.getById({ segmentId: filteredSegs[i], }); if (!conSegObj) continue; if (conSegObj.roadType !== segType) { forwardLock = Math.max(conSegObj.lockRank ?? 0, forwardLock ?? 0); } else { if (!checkedSegs.includes(conSegObj.id)) { const tempRank = processForNode(conSegObj.id); forwardLock = Math.max(tempRank ?? 0, forwardLock ?? 0); } } } return forwardLock ?? 0; } function processRevNode(reverseID) { checkedSegs.push(reverseID); const seg = wmeSDK.DataModel.Segments.getById({ segmentId: reverseID }); if (!seg) return reverseLock; const revNodeId = seg.fromNodeId; if (!revNodeId) return reverseLock; // Get all segments connected to this node const allSegs = wmeSDK.DataModel.Segments.getAll(); const revNodeSegs = allSegs.filter((s) => s.fromNodeId === revNodeId || s.toNodeId === revNodeId).map((s) => s.id); // Remove the current segment from the list const filteredSegs = revNodeSegs.filter((id) => id !== reverseID); for (let i = 0; i < filteredSegs.length; i++) { const conSegObj = wmeSDK.DataModel.Segments.getById({ segmentId: filteredSegs[i], }); if (!conSegObj) continue; if (conSegObj.roadType !== segType) { reverseLock = Math.max(conSegObj.lockRank ?? 0, reverseLock ?? 0); } else { if (!checkedSegs.includes(conSegObj.id)) { const tempRank = processRevNode(conSegObj.id); reverseLock = Math.max(tempRank ?? 0, reverseLock ?? 0); } } } return reverseLock ?? 0; } let calculatedLock = Math.max(processForNode(segID), processRevNode(segID)); return Math.min(calculatedLock, 6); // Limit to L6 } function pushCityNameAlert(cityId, alertMessageParts) { let cityName = ''; if (cityId) { const city = wmeSDK.DataModel.Cities.getById({ cityId }); cityName = city && city.name ? city.name : ''; } alertMessageParts.push(`City Name: <b>${cityName || 'None'}</b>`); } // Helper: Returns true if the roadType is Footpath, Pedestrianised Area, or Stairway function isPedestrianType(roadType) { return [5, 10, 16].includes(roadType); } // Helper: If switching between pedestrian and non-pedestrian types, delete and recreate the segment function recreateSegmentIfNeeded(segmentId, targetRoadType, copyConnectedNameData) { const seg = wmeSDK.DataModel.Segments.getById({ segmentId }); if (!seg) return segmentId; const currentIsPed = isPedestrianType(seg.roadType); const targetIsPed = isPedestrianType(targetRoadType); if (currentIsPed !== targetIsPed) { // Show confirmation dialog before swapping let swapMsg = currentIsPed ? 'You are about to convert a Pedestrian type segment (Footpath, Pedestrianised Area, or Stairway) to a regular street type. This will delete and recreate the segment. Continue?' : 'You are about to convert a regular street segment to a Pedestrian type (Footpath, Pedestrianised Area, or Stairway). This will delete and recreate the segment. Continue?'; if (!window.confirm(swapMsg)) { return null; // Cancel operation } // Save geometry and address const geometry = seg.geometry; const oldPrimaryStreetId = seg.primaryStreetId; const oldAltStreetIds = seg.alternateStreetIds; try { wmeSDK.DataModel.Segments.deleteSegment({ segmentId }); } catch (ex) { if (ex instanceof wmeSDK.Errors.InvalidStateError) { if (WazeWrap?.Alerts) { WazeWrap.Alerts.error('EZRoads Mod Beta', 'Segment could not be deleted. Please check for restrictions or junctions.'); } return null; } } // Create new segment const newSegmentId = wmeSDK.DataModel.Segments.addSegment({ geometry, roadType: targetRoadType }); // Ensure primaryStreetId is valid (not null or undefined) let validPrimaryStreetId = oldPrimaryStreetId; if (!validPrimaryStreetId) { // Use a blank street in the current city let segCityId = getTopCity()?.id; if (!segCityId) { // fallback to country if city is not available segCityId = getCurrentCountry()?.id; } let blankStreet = wmeSDK.DataModel.Streets.getStreet({ cityId: segCityId, streetName: '', }); if (!blankStreet) { blankStreet = wmeSDK.DataModel.Streets.addStreet({ streetName: '', cityId: segCityId, }); } validPrimaryStreetId = blankStreet.id; } // Restore address with valid primaryStreetId wmeSDK.DataModel.Segments.updateAddress({ segmentId: newSegmentId, primaryStreetId: validPrimaryStreetId, alternateStreetIds: oldAltStreetIds, }); // If we have connected segment name data to copy, apply it now if (copyConnectedNameData && copyConnectedNameData.primaryStreetId) { wmeSDK.DataModel.Segments.updateAddress({ segmentId: newSegmentId, primaryStreetId: copyConnectedNameData.primaryStreetId, alternateStreetIds: copyConnectedNameData.alternateStreetIds || [], }); } // Reselect new segment wmeSDK.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } }); return newSegmentId; } return segmentId; } const handleUpdate = () => { const selection = wmeSDK.Editing.getSelection(); if (!selection || selection.objectType !== 'segment') return; log('Updating RoadType'); const options = getOptions(); let alertMessageParts = []; let updatedRoadType = false; let updatedLockLevel = false; let updatedSpeedLimit = false; let updatedPaved = false; let updatedCityName = false; let updatedSegmentName = false; const updatePromises = []; // If copySegmentAttributes is checked, copy all attributes from a connected segment if (options.copySegmentAttributes && !window.suppressCopySegmentAttributes) { selection.ids.forEach((id) => { updatePromises.push( delayedUpdate(() => { try { const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id }); const fromNode = seg.fromNodeId; const connectedSegIds = getConnectedSegmentIDs(id); // Gather all segments connected to fromNode (excluding self) const fromNodeSegs = connectedSegIds.map((sid) => wmeSDK.DataModel.Segments.getById({ segmentId: sid })).filter((s) => s && (s.fromNodeId === fromNode || s.toNodeId === fromNode) && s.id !== id); // Prefer the first fromNode segment with a valid primary street name (and optionally other attributes) let preferredSeg = fromNodeSegs.find((s) => { if (!s) return false; const street = wmeSDK.DataModel.Streets.getById({ streetId: s.primaryStreetId }); return street && street.name; }); let segsToTry = []; if (preferredSeg) { segsToTry.push(preferredSeg.id); // Add the rest, excluding preferredSeg.id segsToTry = segsToTry.concat(connectedSegIds.filter((cid) => cid !== preferredSeg.id)); } else { segsToTry = connectedSegIds; } let found = false; for (let connectedSegId of segsToTry) { const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId }); if (!connectedSeg) continue; const street = wmeSDK.DataModel.Streets.getById({ streetId: connectedSeg.primaryStreetId }); if (street && street.name) { wmeSDK.DataModel.Segments.updateSegment({ segmentId: id, fwdSpeedLimit: connectedSeg.fwdSpeedLimit, revSpeedLimit: connectedSeg.revSpeedLimit, roadType: connectedSeg.roadType, lockRank: connectedSeg.lockRank, }); wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: connectedSeg.primaryStreetId, alternateStreetIds: connectedSeg.alternateStreetIds || [], }); // Copy paved/unpaved const isUnpaved = connectedSeg.flagAttributes && connectedSeg.flagAttributes.unpaved === true; let toggled = false; const segPanel = openPanel; if (segPanel) { const unpavedIcon = segPanel.querySelector('.w-icon-unpaved-fill'); if (unpavedIcon) { const unpavedChip = unpavedIcon.closest('wz-checkable-chip'); if (unpavedChip) { if (isUnpaved !== (seg.flagAttributes && seg.flagAttributes.unpaved === true)) { unpavedChip.click(); toggled = true; } } } if (!toggled) { try { const wzCheckbox = segPanel.querySelector('wz-checkbox[name="unpaved"]'); if (wzCheckbox) { const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]'); if (hiddenInput && hiddenInput.checked !== isUnpaved) { hiddenInput.click(); toggled = true; } } } catch (e) { log('Fallback to non-compact mode unpaved toggle method failed: ' + e); } } } alertMessageParts.push(`Copied all attributes from connected segment.`); found = true; break; } } // If no connected segment with valid street name was found, fallback to any connected segment (like the other logic) if (!found) { let fallbackSegId = null; const segObj = wmeSDK.DataModel.Segments.getById({ segmentId: id }); const fromNode = segObj.fromNodeId; const toNode = segObj.toNodeId; const allSegs = wmeSDK.DataModel.Segments.getAll(); for (let s of allSegs) { if (s.id !== id && (s.fromNodeId === fromNode || s.toNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === toNode)) { fallbackSegId = s.id; break; } } if (fallbackSegId) { const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: fallbackSegId }); wmeSDK.DataModel.Segments.updateSegment({ segmentId: id, fwdSpeedLimit: connectedSeg.fwdSpeedLimit, revSpeedLimit: connectedSeg.revSpeedLimit, roadType: connectedSeg.roadType, lockRank: connectedSeg.lockRank, }); wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: connectedSeg.primaryStreetId, alternateStreetIds: connectedSeg.alternateStreetIds || [], }); // Copy paved/unpaved const isUnpaved = connectedSeg.flagAttributes && connectedSeg.flagAttributes.unpaved === true; let toggled = false; const segPanel = openPanel; if (segPanel) { const unpavedIcon = segPanel.querySelector('.w-icon-unpaved-fill'); if (unpavedIcon) { const unpavedChip = unpavedIcon.closest('wz-checkable-chip'); if (unpavedChip) { if (isUnpaved !== (seg.flagAttributes && seg.flagAttributes.unpaved === true)) { unpavedChip.click(); toggled = true; } } } if (!toggled) { try { const wzCheckbox = segPanel.querySelector('wz-checkbox[name="unpaved"]'); if (wzCheckbox) { const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]'); if (hiddenInput && hiddenInput.checked !== isUnpaved) { hiddenInput.click(); toggled = true; } } } catch (e) { log('Fallback to non-compact mode unpaved toggle method failed: ' + e); } } } alertMessageParts.push(`Copied all attributes from connected segment.`); log(`Copied all attributes from connected segment (fallback, no valid street name).`); } else { alertMessageParts.push(`No connected segment found to copy attributes.`); } } } catch (error) { console.error('Error copying all attributes:', error); } }, 100) ); }); Promise.all(updatePromises).then(() => { if (alertMessageParts.length) { if (WazeWrap?.Alerts) { WazeWrap.Alerts.info('EZRoads Mod', alertMessageParts.join('<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; } selection.ids.forEach((origId, idx) => { let id = origId; let copyConnectedNameData = null; // --- Pedestrian type switching logic --- if (options.roadType) { // If copySegmentName is enabled and switching Street → Pedestrian, prefetch connected segment name const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id }); const currentIsPed = isPedestrianType(seg.roadType); const targetIsPed = isPedestrianType(options.roadType); if (!currentIsPed && targetIsPed && options.copySegmentName) { // Find connected segment and store its name info const fromNode = seg.fromNodeId; const toNode = seg.toNodeId; let connectedSegId = null; const allSegs = wmeSDK.DataModel.Segments.getAll(); for (let s of allSegs) { if (s.id !== id && (s.fromNodeId === fromNode || s.toNodeId === fromNode || s.fromNodeId === toNode || s.toNodeId === toNode)) { connectedSegId = s.id; break; } } if (connectedSegId) { const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId }); copyConnectedNameData = { primaryStreetId: connectedSeg.primaryStreetId, alternateStreetIds: connectedSeg.alternateStreetIds || [], }; } } const newId = recreateSegmentIfNeeded(id, options.roadType, copyConnectedNameData); if (!newId) return; // If failed, skip further updates if (newId !== id) { id = newId; // Use the new segment ID for further updates } } // Road Type updatePromises.push( delayedUpdate(() => { if (options.roadType) { const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id }); const selectedRoad = roadTypes.find((rt) => rt.value === options.roadType); //alertMessageParts.push(`Road Type: <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 (undefined) const speedToSet = !isNaN(speedValue) && speedValue > 0 ? speedValue : undefined; const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id, }); if (seg.fwdSpeedLimit !== speedToSet || seg.revSpeedLimit !== speedToSet) { wmeSDK.DataModel.Segments.updateSegment({ segmentId: id, fwdSpeedLimit: speedToSet, revSpeedLimit: speedToSet, }); alertMessageParts.push(`Speed Limit: <b>${speedToSet !== undefined ? 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 !== undefined ? 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: add available city name automatically city = getTopCity() || getEmptyCity(); } // --- Street assignment logic --- if (options.setStreet) { // Set street name to none and remove all alt street names street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName: '', }); if (!street) { street = wmeSDK.DataModel.Streets.addStreet({ streetName: '', cityId: city.id, }); } // Remove all alternate street names wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: street.id, alternateStreetIds: [], }); } else if (options.setStreetCity) { // Use the same street name as current, but in the empty city for both primary and all alts const currentStreet = segment && segment.primaryStreetId ? wmeSDK.DataModel.Streets.getById({ streetId: segment.primaryStreetId, }) : null; const streetName = currentStreet ? currentStreet.name || '' : ''; street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName: streetName, }); if (!street) { street = wmeSDK.DataModel.Streets.addStreet({ streetName: streetName, cityId: city.id, }); } // For all alternate street names, set them to the empty city as well let newAltStreetIds = []; if (segment && segment.alternateStreetIds) { segment.alternateStreetIds.forEach((altStreetId) => { const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altStreetId }); if (altStreet && altStreet.name !== undefined) { let altInCity = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName: altStreet.name || '', }); if (!altInCity) { altInCity = wmeSDK.DataModel.Streets.addStreet({ streetName: altStreet.name || '', cityId: city.id, }); } newAltStreetIds.push(altInCity.id); } }); } wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: street.id, alternateStreetIds: newAltStreetIds, }); pushCityNameAlert(city.id, alertMessageParts); updatedCityName = true; } else { // If both setStreet and setStreetCity are unchecked, always update city for primary and alt names if (segment && (segment.primaryStreetId || (segment.alternateStreetIds && segment.alternateStreetIds.length))) { // Update primary street to new city let currentStreet = segment.primaryStreetId ? wmeSDK.DataModel.Streets.getById({ streetId: segment.primaryStreetId }) : null; let streetName = currentStreet ? currentStreet.name || '' : ''; street = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName }); if (!street) { street = wmeSDK.DataModel.Streets.addStreet({ streetName, cityId: city.id }); } // Update alt streets to new city let newAltStreetIds = []; if (segment && segment.alternateStreetIds && city) { segment.alternateStreetIds.forEach((altStreetId) => { const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altStreetId }); if (altStreet && altStreet.name !== undefined) { let altInCity = wmeSDK.DataModel.Streets.getStreet({ cityId: city.id, streetName: altStreet.name || '', }); if (!altInCity) { altInCity = wmeSDK.DataModel.Streets.addStreet({ streetName: altStreet.name || '', cityId: city.id, }); } newAltStreetIds.push(altInCity.id); } }); } wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: street.id, alternateStreetIds: newAltStreetIds.length > 0 ? newAltStreetIds : undefined, }); } else { // New/empty street fallback let autoCity = getTopCity() || getEmptyCity(); let autoStreet = wmeSDK.DataModel.Streets.getStreet({ cityId: autoCity.id, streetName: '' }); if (!autoStreet) { autoStreet = wmeSDK.DataModel.Streets.addStreet({ streetName: '', cityId: autoCity.id }); } street = autoStreet; city = autoCity; wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: street.id, alternateStreetIds: undefined, }); } } log(`City Name: ${city?.name}, City ID: ${city?.id}, Street ID: ${street?.id}`); } // Updated unpaved handler with SegmentFlagAttributes and fallback updatePromises.push( delayedUpdate(() => { const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id }); const isPedestrian = isPedestrianType(seg.roadType); if (isPedestrian) { // Always set as paved for pedestrian types, regardless of checkbox const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true; let pavedToggled = false; if (isUnpaved) { // Click to set as paved const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill'); if (unpavedIcon) { const unpavedChip = unpavedIcon.closest('wz-checkable-chip'); if (unpavedChip) { unpavedChip.click(); log('Clicked unpaved chip (set to paved for pedestrian type)'); pavedToggled = true; } } if (!pavedToggled) { try { const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]'); if (wzCheckbox) { const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]'); if (hiddenInput && hiddenInput.checked) { hiddenInput.click(); log('Clicked unpaved checkbox (set to paved, non-compact mode, pedestrian type)'); pavedToggled = true; } } } catch (e) { log('Fallback to non-compact mode paved toggle method failed: ' + e); } } if (pavedToggled) { alertMessageParts.push(`Paved Status: <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 }); const fromNode = seg.fromNodeId; const connectedSegIds = getConnectedSegmentIDs(id); // Gather all segments connected to fromNode (excluding self) const fromNodeSegs = connectedSegIds.map((sid) => wmeSDK.DataModel.Segments.getById({ segmentId: sid })).filter((s) => s && (s.fromNodeId === fromNode || s.toNodeId === fromNode) && s.id !== id); // Prefer the first fromNode segment with a name/city/alias let preferredSeg = fromNodeSegs.find((s) => { if (!s) return false; const street = wmeSDK.DataModel.Streets.getById({ streetId: s.primaryStreetId }); const altStreetIds = s.alternateStreetIds || []; let altNames = []; altStreetIds.forEach((altId) => { const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId }); if (altStreet && altStreet.name) altNames.push(altStreet.name); }); return street && (street.name || street.englishName || street.signText || altNames.length > 0); }); let segsToTry = []; if (preferredSeg) { segsToTry.push(preferredSeg.id); // Add the rest, excluding preferredSeg.id segsToTry = segsToTry.concat(connectedSegIds.filter((cid) => cid !== preferredSeg.id)); } else { segsToTry = connectedSegIds; } let found = false; for (let connectedSegId of segsToTry) { const connectedSeg = wmeSDK.DataModel.Segments.getById({ segmentId: connectedSegId }); if (!connectedSeg) continue; const streetId = connectedSeg.primaryStreetId; const altStreetIds = connectedSeg.alternateStreetIds || []; let street = wmeSDK.DataModel.Streets.getById({ streetId }); // Get alternate street names let altNames = []; altStreetIds.forEach((altId) => { const altStreet = wmeSDK.DataModel.Streets.getById({ streetId: altId }); if (altStreet && altStreet.name) altNames.push(altStreet.name); }); // If any connected segment has a name or alias, use it if (street && (street.name || street.englishName || street.signText || altNames.length > 0)) { if (options.setStreetCity && street) { const emptyCity = wmeSDK.DataModel.Cities.getAll().find((city) => city.isEmpty) || wmeSDK.DataModel.Cities.addCity({ cityName: '' }); // Try to find or create a street with the same name but in the empty city let noneStreet = wmeSDK.DataModel.Streets.getStreet({ cityId: emptyCity.id, streetName: street.name || '', }); if (!noneStreet) { noneStreet = wmeSDK.DataModel.Streets.addStreet({ streetName: street.name || '', cityId: emptyCity.id, }); } wmeSDK.DataModel.Segments.updateAddress({ segmentId: id, primaryStreetId: noneStreet.id, alternateStreetIds: altStreetIds, }); 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 }); Promise.all(updatePromises).then(() => { // Always push city name alert if not already set by other actions selection.ids.forEach((id) => { if (!alertMessageParts.some((part) => part.startsWith('City Name'))) { const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id }); if (seg && seg.primaryStreetId) { const street = wmeSDK.DataModel.Streets.getById({ streetId: seg.primaryStreetId, }); if (street) { pushCityNameAlert(street.cityId, alertMessageParts); updatedCityName = true; } } } }); const showAlert = () => { const updatedFeatures = []; if (updatedCityName) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('City'))); if (updatedSegmentName) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Copied Name'))); if (updatedRoadType) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Road Type'))); if (updatedLockLevel) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Lock Level'))); if (updatedSpeedLimit) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Speed Limit'))); if (updatedPaved) updatedFeatures.push(alertMessageParts.find((part) => part.startsWith('Paved'))); const message = updatedFeatures.filter(Boolean).join(', '); if (message) { if (WazeWrap?.Alerts) { WazeWrap.Alerts.info('EZRoads Mod', `Segment updated with: ${message}`, false, false, 5000); } else { alert('EZRoads Mod: Segment updated (WazeWrap Alerts not available)'); } } }; // Autosave - DELAYED AUTOSAVE if (options.autosave) { setTimeout(() => { log('Delayed Autosave starting...'); wmeSDK.Editing.save().then(() => { log('Delayed Autosave completed.'); showAlert(); }); }, 600); // 1000ms (1 second) delay before autosave } else { showAlert(); } }); }; const constructSettings = () => { const localOptions = getOptions(); let currentRoadType = localOptions.roadType; const update = (key, value) => { const options = getOptions(); options[key] = value; localOptions[key] = value; saveOptions(options); }; // Update lock level for a specific road type const updateLockLevel = (roadTypeId, lockLevel) => { const options = getOptions(); const lockIndex = options.locks.findIndex((l) => l.id === roadTypeId); if (lockIndex !== -1) { options.locks[lockIndex].lock = lockLevel; // Keep as string to handle 'HRCS' localOptions.locks = options.locks; saveOptions(options); if (WazeWrap?.Alerts) { WazeWrap.Alerts.success('EZRoads Mod', 'Lock Levels saved!', false, false, 1500); } else { alert('EZRoads Mod: Lock Levels saved!'); } } }; // Update speed for a specific road type const updateSpeed = (roadTypeId, speed) => { const options = getOptions(); const speedIndex = options.speeds.findIndex((s) => s.id === roadTypeId); let speedValue = parseInt(speed, 10); if (isNaN(speedValue)) { speedValue = -1; } log(`Updating speed for road type ${roadTypeId} to ${speedValue}`); if (speedIndex !== -1) { options.speeds[speedIndex].speed = speedValue; localOptions.speeds = options.speeds; saveOptions(options); if (WazeWrap?.Alerts) { WazeWrap.Alerts.success('EZRoads Mod', 'Speed Values saved!', false, false, 1500); } else { alert('EZRoads Mod: Speed Values saved!'); } } }; // Reset all options to defaults const resetOptions = () => { saveOptions(defaultOptions); // Refresh the page to reload settings window.location.reload(); }; // Checkbox option definitions const checkboxOptions = [ { id: 'setStreet', text: 'Set Street Name to None', key: 'setStreet', tooltip: 'Sets the street name to None for selected segments. If unchecked, leaves the street name unchanged.', }, { id: 'setStreetCity', text: 'Set city as none (uncheck to add auto)', key: 'setStreetCity', tooltip: 'If checked, sets the city to None for selected segments for both primary and alternate streets.. If unchecked, adds the available city name automatically to both primary and alt streets.', }, { id: 'autosave', text: 'Autosave on Action', key: 'autosave', tooltip: 'Automatically saves after updating segments.', }, { id: 'unpaved', text: 'Set as Unpaved (Uncheck for Paved)', key: 'unpaved', tooltip: 'Sets the segment as unpaved. Uncheck to set as paved.', }, { id: 'setLock', text: 'Set the lock level', key: 'setLock', tooltip: 'Sets the lock level for the selected road type. It also enables the lock level dropdown.', }, { id: 'updateSpeed', text: 'Update speed limits', key: 'updateSpeed', tooltip: 'Updates the speed limit for the selected road type. it also enables the speed input field.', }, { id: 'copySegmentName', text: 'Copy connected Segment Name', key: 'copySegmentName', tooltip: "Copies the name and city from a connected segment to the selected segment. If 'Set city as none' is enabled, the city will be set to none regardless of the copied value.", }, { id: 'copySegmentAttributes', text: 'Copy Connected Segment Attribute', key: 'copySegmentAttributes', tooltip: 'Copies all major attributes (road type, lock level, speed limits, paved/unpaved status, primary and alternate street names, and city) from a connected segment. When enabled, it overrides all other options except Autosave. Use shortcut key or (Quick Update Segment) to apply.', }, ]; // Helper function to create radio buttons const createRadioButton = (roadType) => { const id = `road-${roadType.id}`; const isChecked = localOptions.roadType === roadType.value; const lockSetting = localOptions.locks.find((l) => l.id === roadType.id) || { id: roadType.id, lock: 1 }; const speedSetting = localOptions.speeds.find((s) => s.id === roadType.id) || { id: roadType.id, speed: 40 }; const div = $(`<div class="ezroadsmod-option"> <div class="ezroadsmod-radio-container"> <input type="radio" id="${id}" name="defaultRoad" data-road-value="${roadType.value}" ${isChecked ? 'checked' : ''}> <label for="${id}">${roadType.name}</label> <select id="lock-level-${roadType.id}" class="road-lock-level" data-road-id="${roadType.id}" ${!localOptions.setLock ? 'disabled' : ''}> ${locks.map((lock) => `<option value="${lock.value}" ${lockSetting.lock === lock.value ? 'selected' : ''}>${lock.value === 'HRCS' ? 'HRCS' : 'L' + lock.value}</option>`).join('')} </select> <input type="number" id="speed-${roadType.id}" class="road-speed" data-road-id="${roadType.id}" value="${speedSetting.speed}" min="-1" ${!localOptions.updateSpeed ? 'disabled' : ''}> </div> </div>`); div.find('input[type="radio"]').on('click', () => { update('roadType', roadType.value); currentRoadType = roadType.value; }); div.find('select').on('change', function () { updateLockLevel(roadType.id, $(this).val()); }); div.find('input.road-speed').on('change', function () { // Get the value as a number const speedValue = parseInt($(this).val(), 10); // If it's not a number, reset to 0 if (isNaN(speedValue)) { $(this).val(0); updateSpeed(roadType.id, 0); } else { updateSpeed(roadType.id, speedValue); } }); return div; }; // Helper function to create checkboxes const createCheckbox = (option) => { const isChecked = localOptions[option.key]; const otherClass = option.key !== 'autosave' && option.key !== 'copySegmentAttributes' ? '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') { // If any other checkbox (except autosave) 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 update(option.key, $(`#${option.id}`).prop('checked')); } }); 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; } .ezroadsmod-option { margin-bottom: 8px; } .ezroadsmod-radio-container { display: flex; align-items: center; } .ezroadsmod-radio-container input[type="radio"] { margin-right: 5px; } .ezroadsmod-radio-container label { flex: 1; margin-right: 10px; text-align: left; } .ezroadsmod-radio-container select { width: 80px; margin-left: auto; margin-right: 5px; } .ezroadsmod-radio-container input.road-speed { width: 60px; } .ezroadsmod-reset-button { margin-top: 20px; padding: 8px 12px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } .ezroadsmod-reset-button:hover { background-color: #d32f2f; } </style>`); tabPane.innerHTML = '<div id="ezroadsmod-settings"></div>'; const scriptContentPane = $('#ezroadsmod-settings'); scriptContentPane.append(styles); // Header section const header = $(`<div class="ezroadsmod-section"> <h2>EZRoads Mod</h2> <div>Current Version: <b>${scriptVersion}</b></div> <div>Update Keybind: <kbd>G</kbd></div> <div style="font-size: 0.8em;">You can change it in WME keyboard setting!</div> </div>`); scriptContentPane.append(header); // Road type and options header const roadTypeHeader = $(`<div class="ezroads-section"> <div style="display: flex; align-items: center;"> <div style="flex-grow: 1; text-align: center;">Road Type</div> <div style="width: 80px; text-align: center;">Lock</div> <div style="width: 60px; text-align: center;">Speed</div> </div> </div>`); scriptContentPane.append(roadTypeHeader); // Road type section with header const roadTypeSection = $(`<div class="ezroads-section"> <div id="road-type-options"></div> </div>`); scriptContentPane.append(roadTypeSection); const roadTypeOptions = roadTypeSection.find('#road-type-options'); roadTypes.forEach((roadType) => { roadTypeOptions.append(createRadioButton(roadType)); }); // Additional options section const additionalSection = $(`<div class="ezroadsmod-section"> <h5>Additional Options</h5> <div id="additional-options"></div> </div>`); scriptContentPane.append(additionalSection); const additionalOptions = additionalSection.find('#additional-options'); checkboxOptions.forEach((option) => { additionalOptions.append(createCheckbox(option)); }); // Update all lock dropdowns when setLock checkbox changes $(document).on('click', '#setLock', function () { const isChecked = $(this).prop('checked'); $('.road-lock-level').prop('disabled', !isChecked); }); // Update all speed inputs when updateSpeed checkbox changes $(document).on('click', '#updateSpeed', function () { const isChecked = $(this).prop('checked'); $('.road-speed').prop('disabled', !isChecked); log('Speed update option changed to: ' + isChecked); }); // Reset button section const resetButton = $(`<button class="ezroadsmod-reset-button">Reset All Options</button>`); resetButton.on('click', function () { if (confirm('Are you sure you want to reset all options to default values? It will reload the webpage!')) { resetOptions(); } }); scriptContentPane.append(resetButton); // --- Export/Import Config UI --- const exportImportSection = $( `<div class="ezroadsmod-section" style="margin-top:10px;"> <button id="ezroadsmod-export-btn" style="margin-right:8px;">Export Lock/Speed Config</button> <button id="ezroadsmod-import-btn">Import Lock/Speed Config</button> <input id="ezroadsmod-import-input" type="text" placeholder="Paste config here" style="width:60%;margin-left:8px;"> </div>` ); scriptContentPane.append(exportImportSection); // Export logic $(document).on('click', '#ezroadsmod-export-btn', function () { const options = getOptions(); const exportData = { locks: options.locks, speeds: options.speeds, }; const exportStr = JSON.stringify(exportData, null, 2); // Copy to clipboard navigator.clipboard.writeText(exportStr).then( () => { if (WazeWrap?.Alerts) { WazeWrap.Alerts.success('EZRoads Mod', 'Lock/Speed config copied to clipboard!', false, false, 2000); } else { alert('Lock/Speed config copied to clipboard!'); } }, () => { alert('Failed to copy config to clipboard.'); } ); }); // Import logic $(document).on('click', '#ezroadsmod-import-btn', function () { const importStr = $('#ezroadsmod-import-input').val(); if (!importStr) { alert('Please paste a config string to import.'); return; } let importData; try { importData = JSON.parse(importStr); } catch (e) { alert('Invalid config string!'); return; } if (importData.locks && importData.speeds) { const options = getOptions(); options.locks = importData.locks; options.speeds = importData.speeds; saveOptions(options); // Update in-memory localOptions and UI localOptions.locks = importData.locks; localOptions.speeds = importData.speeds; // 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 (WazeWrap?.Alerts) { WazeWrap.Alerts.success('EZRoads Mod', 'Config imported and applied!', false, false, 2000); } else { alert('Config imported and applied!'); } } else { alert('Config missing lock/speed data!'); } }); }); }; function scriptupdatemonitor() { if (WazeWrap?.Ready) { bootstrap({ scriptUpdateMonitor: { downloadUrl } }); WazeWrap.Interface.ShowScriptUpdate(scriptName, scriptVersion, updateMessage, downloadUrl, forumURL); } else { setTimeout(scriptupdatemonitor, 250); } } // Start the "scriptupdatemonitor" scriptupdatemonitor(); console.log(`${scriptName} initialized.`); /* Change Log Version 2.5.9.4 - 2025-07-15 - Enhanced "Copy Connected Segment Attribute" logic: reliably copies from segments with valid street names, falls back to any connected segment if needed. - Added more robust paved/unpaved attribute copying. 2.5.9.3 - 2025-07-04 - Updated logic for speed limit. Now it will not update speed limit set to 0 or -1. 2.5.9.2 - 2025-07-03-01 - improved logic for copy first connected segment name and/or city. - Able to work with road type button in compact mode. <b>2.5.9 - 2025-06-23</b><br> - Added a confirmation popup before changing between Street and Footpath/Pedestrianised Area/Stairway types.<br> - If you cancel, the segment will not be changed.<br> - Prevents accidental deletion and recreation of special segment types.<br> - Segment name copying now works for both one-way and two-way segments when converting to pedestrian types.<br> - When "Copy connected Segment Name" is on, the script copies the name from the connected segment before conversion and applies it after.<br> - Improved reliability for segment recreation and name copying.<br> - Includes all previous improvements and bug fixes.<br> 2.5.8 - 2025-06-21</b><br> - When "Set Street Name to None" is checked, the primary street is set to none and all alternate street names are removed.<br> - When "Set city as none" is checked, all primary and alternate city names are set to none (empty city).<br> - "Set street to none" now only handles the street name.<br> - Added option for setting city to none.<br> - Lock and speed settings can be exported or imported.<br> - In case of shortcut key conflict, the shortcut key becomes null.<br> - Default shortcut key is now "G".<br> - Minor code cleanup.<br> - Other behaviors remain unchanged.<br> 2.5.7.4 - 2025-06-15 - Set street to none only handles street name now. - Added option for setting up city to none. - The lock and speed setting can be exported or imported. - Incase of the shortcutkey conflict, the shortcut key becomes null. - Default shortcut key is now "G". - Minor code cleanup. 2.5.7 - 2025-06-13 - Fixed: Autosave now works when "Copy Connected Segment Attribute" is enabled. - When both "Autosave on Action" and "Copy Connected Segment Attribute" are checked, pressing the quick update button or shortcut will copy attributes and then autosave. - All other options continue to function as before. 2.5.6 - 2025-06-08.02 - minor bugfixes and UI improvements. 2.5.5 - 2025-06-08.01 - Improved mutual exclusion logic for "Copy Connected Segment Attribute" and other checkboxes (except Autosave): - Checking "Copy Connected Segment Attribute" now unchecks all other options (except Autosave), but does not disable them. - Checking any other option (except Autosave) will uncheck "Copy Connected Segment Attribute". - Added tooltips for all checkbox options. - Fixed radio button selection logic for road types when using keyboard shortcuts. - Minor bugfixes and UI improvements. 2.5.5 - 2025-06-08.01 - Minor bugfixes 2.5.4 - 2025-06-07.01 - Road Types can be selected using keyboard shortcuts. - Fully migrated to WME SDK. 2.5.3 - 2025-05-31 - Improved shortcut keybind logic and UI. - The unpaved attribute is now copied in Non-compact mode. - Minor bug fixes. 2.5.2 - 2025-05-31 - Improved shortcut keybind logic and UI. - Prevented double alerts and unnecessary alerts when keybind is empty. - Minor bug fixes. 2.5.1 - 2025-05-30 - Added "Copy Connected Segment Attribute" option. - When enabled, all other options (except Autosave) are automatically unchecked. - Copies all major attributes from a connected segment: speed limit, primary name, alias name, city, paved/unpaved status, and lock level. - Ensures mutually exclusive logic for this option in the UI. 2.5.0 - 2025-05-29 - Improved reliability of the unpaved toggle by adding a 500ms delay and fallback logic for both compact and non-compact UI modes. - Now ensures paved/unpaved status is always set correctly, regardless of UI mode. - Enhanced update logic: skips redundant updates for road type, lock, and speed if already set. - Improved city name and segment name copying logic, with more robust alerts and feedback. - UI improvements: disables lock/speed controls when their options are off; mutually exclusive logic for "Set Street To None" and "Copy connected Segment Name". - Improved logging for easier debugging and tracking. - Bug fixes and code cleanup for better reliability and maintainability. 2.4.9 - 2025-05-22 - Improved reliability of unpaved toggle (500ms delay). - Minor bug fixes and code cleanup 2.4.8 - 2025-05-21 - Now pave/unpave works better. When unchecked, the segment will be paved! 2.4.7 - 2025-05-21 Added support for copying alternative segment name from connected segment Thanks to - - MapOmatic, without him this would not be possible. 2.4.6 - 2025-05-20 Added support for copying segment name from connected segment Limitation - - Currently Alt names wont be copied. 2.4.5 - 2025-05-11 Added Support for HRCS Fixed - - Various other bugs */ })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址