// ==UserScript==
// @name WME EZRoad Mod
// @namespace https://greasyfork.org/users/1087400
// @version 2.4.8
// @description Easily update roads
// @author https://github.com/michaelrosstarr, https://greasyfork.org/en/users/1087400-kid4rm90s
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @exclude https://www.waze.com/user/*editor/*
// @exclude https://www.waze.com/*/user/*editor/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant unsafeWindow
// @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @license GNU GPL(v3)
// @connect greasyfork.org
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require https://update.greasyfork.icu/scripts/509664/WME%20Utils%20-%20Bootstrap.js
// @downloadURL none
// ==/UserScript==
/*Script modified from WME EZRoad (https://greasyfork.org/en/scripts/518381-wme-ezsegments) original author: Michaelrosstarr and thanks to him*/
(function main() {
"use strict";
const updateMessage = 'Now pave/unpave works better.
When unchecked, the segment will be paved!';
const scriptName = GM_info.script.name;
const scriptVersion = GM_info.script.version;
const downloadUrl = 'https://greasyfork.org/scripts/528552-wme-ezroad-mod/code/wme-ezroad-mod.user.js';
let wmeSDK;
const roadTypes = [
{ id: 1, name: 'Motorway', value: 3 },
{ id: 2, name: 'Ramp', value: 4 },
{ id: 3, name: 'Major Highway', value: 6 },
{ id: 4, name: 'Minor Highway', value: 7 },
{ id: 5, name: 'Primary Street', value: 2 },
{ id: 6, name: 'Street', value: 1 },
{ id: 7, name: 'Narrow Street', value: 22 },
{ id: 8, name: 'Offroad', value: 8 },
{ id: 9, name: 'Parking Road', value: 20 },
{ id: 10, name: 'Private Road', value: 17 },
{ id: 11, name: 'Ferry', value: 15 },
{ id: 12, name: 'Railroad', value: 18 },
{ id: 13, name: 'Runway/Taxiway', value: 19 },
{ id: 14, name: 'Foothpath', value: 5 },
{ id: 15, name: 'Pedestrianised Area', value: 10 },
{ id: 16, name: 'Stairway', value: 16 },
];
const defaultOptions = {
roadType: 1,
unpaved: false,
setStreet: false,
autosave: false,
setSpeed: 40,
setLock: false,
updateSpeed: false,
copySegmentName: false,
locks: roadTypes.map(roadType => ({ id: roadType.id, lock: 1 })),
speeds: roadTypes.map(roadType => ({ id: roadType.id, speed: 40 }))
};
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();
}
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')) || {};
// Merge saved options with defaults to ensure all expected options exist
return { ...defaultOptions, ...savedOptions };
}
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 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;
// Check if THIS SPECIFIC panel already has the button
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");
}
}
}
}
});
});
roadObserver.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });
constructSettings();
document.addEventListener("keydown", (event) => {
// Check if the active element is an input or textarea
const isInputActive = document.activeElement && (
document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA' ||
document.activeElement.contentEditable === 'true' ||
document.activeElement.tagName === 'WZ-AUTOCOMPLETE' ||
document.activeElement.tagName === 'WZ-TEXTAREA'
);
log(document.activeElement.tagName);
log(isInputActive);
// Only trigger the update if the active element is not an input or textarea
if (!isInputActive && event.key.toLowerCase() === "g") {
handleUpdate();
}
});
log("Completed Init")
}
const getEmptyStreet = () => {
}
const getEmptyCity = () => {
return wmeSDK.DataModel.Cities.getCity({
cityName: '',
countryId: getCurrentCountry().id
}) || wmeSDK.DataModel.Cities.addCity({
cityName: '',
countryId: getCurrentCountry().id
});
}
// Helper function to wrap updates in a promise with delay
const delayedUpdate = (updateFn, delay) => {
return new Promise(resolve => {
setTimeout(() => {
updateFn();
resolve();
}, delay);
});
};
function getHighestSegLock(segID) {
const segObj = W.model.segments.getObjectById(segID);
if (!segObj) {
console.warn(`Segment object with ID ${segID} not found in W.model.segments.`);
return 1; // Default lock level if segment not found
}
const segType = segObj.attributes.roadType;
const checkedSegs = [];
let forwardLock = null; // Initialize to L1
let reverseLock = null; // Initialize to L1
function processForNode(forwardID) {
checkedSegs.push(forwardID);
const forNode = W.model.segments.getObjectById(forwardID).getToNode();
if (!forNode) return forwardLock; // defensive check
const forNodeSegs = [...forNode.attributes.segIDs];
for (let j = 0; j < forNodeSegs.length; j++) {
if (forNodeSegs[j] === forwardID) { forNodeSegs.splice(j, 1); }
}
for (let i = 0; i < forNodeSegs.length; i++) {
const conSegObj = W.model.segments.getObjectById(forNodeSegs[i]);
if (!conSegObj) continue; // Defensive check - skip if segment object is not found
const conSeg = conSegObj.attributes;
if (conSeg.roadType !== segType) {
forwardLock = Math.max(conSeg.lockRank + 0, forwardLock); // +1 to convert lockRank to lock level
} else {
for (let k = 0; k < forNodeSegs.length; k++) {
if (!checkedSegs.some(segNum => segNum === conSeg.id)) {
const tempRank = processForNode(conSeg.id);
forwardLock = Math.max(tempRank, forwardLock);
}
}
}
}
return forwardLock;
}
function processRevNode(reverseID) {
checkedSegs.push(reverseID);
const revNode = W.model.segments.getObjectById(reverseID).getFromNode();
if (!revNode) return reverseLock; // defensive check, return current lock if node is not valid
const revNodeSegs = [...revNode.attributes.segIDs];
for (let j = 0; j < revNodeSegs.length; j++) {
if (revNodeSegs[j] === reverseID) { revNodeSegs.splice(j, 1); }
}
for (let i = 0; i < revNodeSegs.length; i++) {
const conSegObj = W.model.segments.getObjectById(revNodeSegs[i]);
if (!conSegObj) continue; // Defensive check - skip if segment object is not found
const conSeg = conSegObj.attributes;
if (conSeg.roadType !== segType) {
reverseLock = Math.max(conSeg.lockRank + 0, reverseLock); // +1 to convert lockRank to lock level
} else {
for (let k = 0; k < revNodeSegs.length; k++) {
if (!checkedSegs.some(segNum => segNum === conSeg.id)) {
const tempRank = processRevNode(conSeg.id);
reverseLock = Math.max(tempRank, reverseLock);
}
}
}
}
return reverseLock;
}
let calculatedLock = Math.max(processForNode(segID), processRevNode(segID));
return Math.min(calculatedLock, 6); // Limit to L6
}
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 updatedStreet = false;
let updatedPaved = false;
let updatedCityName = false;
let updatedSegmentName = false;
const updatePromises = [];
selection.ids.forEach(id => {
// Road Type
updatePromises.push(delayedUpdate(() => {
if (options.roadType) {
const seg = wmeSDK.DataModel.Segments.getById({segmentId: id});
const selectedRoad = roadTypes.find(rt => rt.value === options.roadType);
//alertMessageParts.push(`Road Type: ${selectedRoad.name}`);
//updatedRoadType = true;
log(`Segment ID: ${id}, Current Road Type: ${seg.roadType}, Target Road Type: ${options.roadType}, Target Road Name : ${selectedRoad.name}`); // Log current and target road type
if (seg.roadType === options.roadType) {
log(`Segment ID: ${id} already has the target road type: ${options.roadType}. Skipping update.`);
alertMessageParts.push(`Road Type: ${selectedRoad.name} exists. Skipping update.`);
updatedRoadType = true;
} else {
try {
wmeSDK.DataModel.Segments.updateSegment({ segmentId: id, roadType: options.roadType });
log('Road type updated successfully.');
alertMessageParts.push(`Road Type: ${selectedRoad.name}`);
updatedRoadType = true;
} catch (error) {
console.error('Error updating road type:', error);
}
}
}
}, 200)); // 200ms delay before road type update
// Set lock if enabled
updatePromises.push(delayedUpdate(() => {
if (options.setLock) {
const rank = wmeSDK.State.getUserInfo().rank;
const selectedRoad = roadTypes.find(rt => rt.value === options.roadType);
if (selectedRoad) {
let lockSetting = options.locks.find(l => l.id === selectedRoad.id);
if (lockSetting) {
let toLock = lockSetting.lock;
if (toLock === 'HRCS') {
toLock = getHighestSegLock(id);
} else {
toLock = parseInt(toLock, 10);
toLock = Math.max(toLock - 1, 0); // Adjust to 0-based rank, ensuring it does not go below 0
}
if (rank < toLock) toLock = rank;
log(toLock);
try {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
let displayLockLevel = (toLock === 'HRCS' || isNaN(toLock)) ? 'HRCS' : `L${toLock + 1}`;
let currentDisplayLockLevel;
if (seg.lockRank === 'HRCS') { // Should not happen, but for safety
currentDisplayLockLevel = 'HRCS';
} //else if (isNaN(seg.lockRank)) {
// currentDisplayLockLevel = 'Unknown'; // Or handle as appropriate
// }
else {
currentDisplayLockLevel = `L${seg.lockRank + 1}`;
}
if (seg.lockRank === toLock || (lockSetting.lock === 'HRCS' && currentDisplayLockLevel === displayLockLevel)) { // Compare lock levels
log(`Segment ID: ${id} already has the target lock level: ${displayLockLevel}. Skipping update.`);
alertMessageParts.push(`Lock Level: ${displayLockLevel} exists. Skipping update.`);
updatedLockLevel = true;
} else {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
lockRank: toLock
});
alertMessageParts.push(`Lock Level: ${displayLockLevel}`);
updatedLockLevel = true;
}
} catch (error) {
console.error('Error updating segment lock rank:', error);
}
}
}
}
}, 300)); // 250ms delay before lock rank update
// Speed Limit - use road-specific speed if updateSpeed is enabled
updatePromises.push(delayedUpdate(() => {
if (options.updateSpeed) {
const selectedRoad = roadTypes.find(rt => rt.value === options.roadType);
if (selectedRoad) {
const speedSetting = options.speeds.find(s => s.id === selectedRoad.id);
log('Selected road for speed: ' + selectedRoad.name);
log('Speed setting found: ' + (speedSetting ? 'yes' : 'no'));
if (speedSetting) {
const speedValue = parseInt(speedSetting.speed, 10);
log('Speed value to set: ' + speedValue);
// Apply speed if it's a valid number (including 0)
if (!isNaN(speedValue) && speedValue >= 0) {
log('Applying speed: ' + speedValue);
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
if (seg.fwdSpeedLimit !== speedValue || seg.revSpeedLimit !== speedValue) {
wmeSDK.DataModel.Segments.updateSegment({
segmentId: id,
fwdSpeedLimit: speedValue,
revSpeedLimit: speedValue
});
alertMessageParts.push(`Speed Limit: ${speedValue}`);
updatedSpeedLimit = true;
} else {
log(`Segment ID: ${id} already has the target speed limit: ${speedValue}. Skipping update.`);
alertMessageParts.push(`Speed Limit: ${speedValue} exists. Skipping update.`);
updatedSpeedLimit = true;
}
//alertMessageParts.push(`Speed Limit: ${speedValue}`);
//updatedSpeedLimit = true;
} else {
log('Not applying speed - invalid value: ' + speedSetting.speed);
alertMessageParts.push(`Speed Limit: Invalid value ${speedValue}`);
updatedSpeedLimit = true;
}
}
}
} else {
log('Speed updates disabled');
}
}, 400)); // 300ms delay before lock rank update
// Handling the street
if (options.setStreet) {
let city;
let street;
city = getTopCity() || getEmptyCity();
street = wmeSDK.DataModel.Streets.getStreet({
cityId: city.id,
streetName: '',
});
//alertMessageParts.push(`City Name: ${city?.name || 'None'}`);
//updatedCityName = true;
log(`City Name: ${city?.name}, City ID: ${city?.id}, Street ID: ${street?.id}`);
if(!street) {
street = wmeSDK.DataModel.Streets.addStreet({
streetName: '',
cityId: city.id
});
log(`Created new empty street. Street ID: ${street?.id}`);
}
try {
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: street.id
});
alertMessageParts.push(`City Name: ${city?.name || 'None'}`);
updatedCityName = true;
} catch (error) {
console.error('Error updating segment address:', error);
}
}
log(options);
// Updated unpaved handler with SegmentFlagAttributes and fallback
if (options.unpaved) {
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let unpavedToggled = false;
if (!isUnpaved) {
// Only click if segment is not already unpaved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to unpaved)');
unpavedToggled = true;
}
}
// If new method failed, try the old method as fallback for non-compact mode
if (!unpavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && !hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to unpaved, non-compact mode)');
unpavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode unpaved toggle method failed: ' + e);
}
}
if (unpavedToggled) {
alertMessageParts.push(`Paved Status: Unpaved`);
updatedPaved = true;
}
} else {
// Already unpaved, no action needed
alertMessageParts.push(`Paved Status: Unpaved (already set)`);
updatedPaved = true;
}
} else {
// If option is not checked and segment is unpaved, set it as paved
const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
const isUnpaved = seg.flagAttributes && seg.flagAttributes.unpaved === true;
let pavedToggled = false;
if (isUnpaved) {
// Click to set as paved
const unpavedIcon = openPanel.querySelector('.w-icon-unpaved-fill');
if (unpavedIcon) {
const unpavedChip = unpavedIcon.closest('wz-checkable-chip');
if (unpavedChip) {
unpavedChip.click();
log('Clicked unpaved chip (set to paved)');
pavedToggled = true;
}
}
// If new method failed, try the old method as fallback for non-compact mode
if (!pavedToggled) {
try {
const wzCheckbox = openPanel.querySelector('wz-checkbox[name="unpaved"]');
if (wzCheckbox) {
const hiddenInput = wzCheckbox.querySelector('input[type="checkbox"][name="unpaved"]');
if (hiddenInput && hiddenInput.checked) {
hiddenInput.click();
log('Clicked unpaved checkbox (set to paved, non-compact mode)');
pavedToggled = true;
}
}
} catch (e) {
log('Fallback to non-compact mode paved toggle method failed: ' + e);
}
}
if (pavedToggled) {
alertMessageParts.push(`Paved Status: Paved`);
updatedPaved = true;
}
} else {
// Already paved, no action needed
alertMessageParts.push(`Paved Status: Paved (already set)`);
updatedPaved = true;
}
}
// 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 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 });
const streetId = connectedSeg.primaryStreetId;
const altStreetIds = connectedSeg.alternateStreetIds || [];
const street = wmeSDK.DataModel.Streets.getById({ streetId });
// Get alternate street names
let altNames = [];
altStreetIds.forEach(streetId => {
const altStreet = wmeSDK.DataModel.Streets.getById({ streetId });
if (altStreet && altStreet.name) altNames.push(altStreet.name);
});
if (street && (street.name || street.englishName || street.signText)) {
// Copy both main and alternate street IDs
wmeSDK.DataModel.Segments.updateAddress({
segmentId: id,
primaryStreetId: streetId,
alternateStreetIds: altStreetIds
});
let aliasMsg = altNames.length ? ` (Alternatives: ${altNames.join(', ')})` : '';
alertMessageParts.push(`Copied Name: ${street.name || ''}${aliasMsg}`);
updatedSegmentName = true;
} else {
alertMessageParts.push(`Copied Name: None (connected segment has no name)`);
}
} else {
alertMessageParts.push(`Copied Name: None (no connected segment found)`);
}
} catch (error) {
console.error('Error copying segment name:', error);
}
}
}, 100)); // Run early in the update chain
})
Promise.all(updatePromises).then(() => {
const showAlert = () => {
const updatedFeatures = [];
if (updatedSegmentName) updatedFeatures.push(alertMessageParts.find(part => part.startsWith("Copied Name")));
if (updatedCityName) updatedFeatures.push(alertMessageParts.find(part => part.startsWith("City")));
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, 7000);
} 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();
});
}, 500); // 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);
}
};
// Update speed for a specific road type
const updateSpeed = (roadTypeId, speed) => {
const options = getOptions();
const speedIndex = options.speeds.findIndex(s => s.id === roadTypeId);
// Make sure we have a valid integer
let speedValue = parseInt(speed, 10);
if (isNaN(speedValue)) {
speedValue = -1; // Default to -1 for invalid values
}
log(`Updating speed for road type ${roadTypeId} to ${speedValue}`);
if (speedIndex !== -1) {
options.speeds[speedIndex].speed = speedValue;
localOptions.speeds = options.speeds;
saveOptions(options);
}
};
// 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 To None', key: 'setStreet' },
{ id: 'autosave', text: 'Autosave on Action', key: 'autosave' },
{ id: 'unpaved', text: 'Set as Unpaved (Uncheck for Paved)', key: 'unpaved' },
{ id: 'setLock', text: 'Set the lock level', key: 'setLock' },
{ id: 'updateSpeed', text: 'Update speed limits', key: 'updateSpeed' },
{ id: 'copySegmentName', text: 'Copy Segment Name from Connected', key: 'copySegmentName' }
];
// 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 = $(`