您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fork of the original WME LevelReset script by Broos Gert '2015. The script is for making re-locking segments and POI to their appropriate lock level easy & quick. Supports all road types, venues and custom locking rules for a specific countries and cities.
// ==UserScript== // @name WME Relock // @version 2025.08.23.001 // @description Fork of the original WME LevelReset script by Broos Gert '2015. The script is for making re-locking segments and POI to their appropriate lock level easy & quick. Supports all road types, venues and custom locking rules for a specific countries and cities. // @author madnut, Copilot // @match https://beta.waze.com/*editor* // @match https://www.waze.com/*editor* // @exclude https://www.waze.com/*user/*editor/* // @namespace https://greasyfork.org/uk/users/160654-waze-ukraine // @connect google.com // @connect script.googleusercontent.com // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant unsafeWindow // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAA+VBMVEX///93PgHX19fTgQfFZgLLcwTrxYDDgA3nqBj5+fmwr6+Yl5f8/PzExMTl5eX114vv7+/e3t68vLzOzs6saRKARQSLTgeioqK2tbX72XfU1NT515fxz4b54b3RmySYWAv31aTpwIHgrn9/f3/75qPZsEvuuC/utx3psVP13KizbhXuuVj745bfoEzzwzDxwDXTjknpxqDPfhzWih7PhUaObErowqDJchrmqCfprRjbmUvblCLZjAv71WnhnyTfmA7hrmbjsm7qxpPv06vYljj305776MvLkD3XkjFwcHCMi4v6zk/6z1P2wVDYqzr3y3j2xWnrrl761X3u0VhGAAABv0lEQVQ4jZWTXXuaMBiGY7bZQUhIoBaKsIK0KkVqtd+2tJ2gnVJs9f//mAW78uHYwe6TXE+em/flJAD8D0RVdF3HTKqvGcaMAiAQVYd1vaEASikhhFKA1ZoeA8Iwct2lCAnAxl/zdcAMbeGipbtwMQM62xFEFUJtoWEIsbh0CVTF3QGqqrjax2cq4kkkFQFjTJD2eYeXBoa4uoEoBOU/RhBUWHWHJukUCZ9JQFCnWkVAQJRQniREyvGPANA/YzazRhBKwjSOg+DZmdoRZ+r8XAfxr5eo1AfzuW1HljXfYkX2zJ5b8TQXXtbWzPff38x2hvn27qf+zFrHubC39tppGoabjczZHIZpmra9/jgXTn2vnSTJaxgecsLwNRkmsueflgV5eLZarU4y+Lk6G9YIg8HxB4PBYEfY3woZQ0529rjQ3y+Evid3ez9K9LpmWTjqe2b3Ti5xlwlHhRDYzdvvFW5NOyiEAy48Pu2VeHps2sFBIUwi5/6hWeLh3okmhdCajJyLLxUunNGktS0lgdLW+agz/lZh3Bmdt6ggZS/NUBqX152brxVuOteXDZVRafsUrxq1XGHIBb6CwHoY4Tt+A1eiQ8S/AAv7AAAAAElFTkSuQmCC // ==/UserScript== /* jshint esversion: 11 */ // eslint-disable-next-line no-redeclare /* global getWmeSdk */ // eslint-disable-next-line no-redeclare /* global W */ (function () { 'use strict'; const ID_KEYS = { MSG_HIDE: 'Relock_msgHide', ALL_SEGMENTS: 'Relock_allSegments', RESPECT_ROUTING: 'Relock_respectRouting', INFO_BOX: 'Relock_infoBox', ELM_PREFIX: 'Relock_', CHECKBOX_SUFFIX: '_checkbox', VALUE_SUFFIX: '_value', ROAD_TYPE_CONTAINER_SUFFIX: '_roadTypeContainer' }; const SCRIPT_ID = GM_info.script.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); const SCRIPT_VERSION = GM_info.script.version; const SCRIPT_LOG_PREFIX = 'Relock:'; const FIX_LIMIT_COUNT = 150; const POI_ID = "90000"; // Fake ID for POI to not conflict with real street IDs const POI_NAME = "POI"; let wmeSDK; let userLevel; // Cache user level to avoid repeated SDK calls const minimumUserLevelForHigherLocks = 4; // Global cached elements object for performance optimization const cachedElements = { relockAllbutton: null, lockColorElement: null, checkboxElements: {}, // Cache for road type checkboxes respectRoutingElement: null, // Cache for respect routing checkbox scanCounterElement: null, // Cache for scan counter element infoBox: null, // Cache for info box element alertContainer: null, // Cache for alert container relockContainer: null, // Cache for main relock container roadTypeContainers: {}, // Cache for road type value containers allSegmentsCheckbox: null, // Cache for all segments checkbox toggleAllCheckboxesIcon: null // Cache for toggle all checkboxes icon }; const REQUEST_TIMEOUT = 20000; // in ms const RULES_HASH = "AKfycbyBy5e4J1u3RbRK4cNWbUJ-sDL2aLDUIMH1glbf6xOEEMO0Z4wl2wTKIRw0HP5KDbwR6A"; // Default lock levels const DEFAULT_STREET_LOCKS = { STREET: 1, PRIMARY_STREET: 1, MINOR_HIGHWAY: 2, MAJOR_HIGHWAY: 3, RAMP: 4, FREEWAY: 4, POI: 1, RAILROAD: 1, PRIVATE_ROAD: 1, PARKING_LOT_ROAD: 1, OFF_ROAD: 1, ALLEY: 1, PEDESTRIAN_BOARDWALK: 1, WALKING_TRAIL: 1, STAIRWAY: 1, WALKWAY: 1, FERRY: 1, RUNWAY_TAXIWAY: 1 }; const ErrorHandler = { SEVERITY: { CRITICAL: 'critical', // Fatal errors that prevent script from working ERROR: 'error', // Errors that affect functionality but allow continuation WARNING: 'warning', // Warnings about potential issues INFO: 'info' // Informational messages }, /** * Central error logging and handling function * @param {Error|string} error - Error object or message * @param {string} context - Context where error occurred (function name, operation) * @param {string} severity - Error severity level * @param {Object} additionalInfo - Additional context information */ handle(error, context = 'Unknown', severity = this.SEVERITY.ERROR, additionalInfo = {}) { const errorMsg = error instanceof Error ? error.message : String(error); const fullMessage = `${SCRIPT_LOG_PREFIX} [${context}] ${errorMsg}`; switch (severity) { case this.SEVERITY.CRITICAL: console.error(fullMessage, error instanceof Error ? error.stack : '', additionalInfo); break; case this.SEVERITY.ERROR: console.error(fullMessage, error instanceof Error ? error.stack : '', additionalInfo); break; case this.SEVERITY.WARNING: console.warn(fullMessage, error instanceof Error ? error.stack : '', additionalInfo); break; case this.SEVERITY.INFO: console.log(fullMessage, additionalInfo); break; default: console.error(fullMessage, error instanceof Error ? error.stack : '', additionalInfo); } if (severity === this.SEVERITY.CRITICAL) { const userMessage = `${SCRIPT_LOG_PREFIX} Critical Error in ${context}\n${errorMsg}\n\nScript may not function properly.`; // eslint-disable-next-line no-alert alert(userMessage); } return false; }, /** * Wrap async functions with error handling * @param {Function} asyncFn - Async function to wrap * @param {string} context - Context for error reporting * @param {string} severity - Default severity level * @returns {Function} - Wrapped function with error handling */ wrapAsync(asyncFn, context, severity = this.SEVERITY.ERROR) { return async (...args) => { try { return await asyncFn(...args); } catch (error) { this.handle(error, context, severity); return null; } }; }, /** * Create a try/catch wrapper for synchronous functions * @param {Function} fn - Function to wrap * @param {string} context - Context for error reporting * @param {string} severity - Default severity level * @param {*} defaultReturn - Default return value on error * @returns {Function} - Wrapped function with error handling */ wrapSync(fn, context, severity = this.SEVERITY.ERROR, defaultReturn = false) { return (...args) => { try { return fn(...args); } catch (error) { this.handle(error, context, severity); return defaultReturn; } }; } }; /** * Async delay utility function * @param {number} ms - Milliseconds to wait * @returns {Promise<void>} */ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** * Animate element visibility with smooth transitions * Replaces jQuery's show/hide animations with native CSS transitions * @param {HTMLElement|string} element - Element or element ID to animate * @param {boolean} show - true to show, false to hide * @param {string} speed - 'fast' (150ms), 'slow' (600ms), or 'normal' (300ms) * @param {string} displayType - CSS display value when showing ('block', 'inline-block', 'table-row', etc.) * @example * animateElement('alertBox', true, 'fast'); // Show element quickly * animateElement(myElement, false, 'slow'); // Hide element slowly * animateElement(row, true, 'normal', 'table-row'); // Show table row */ function animateElement(element, show, speed = 'normal', displayType = 'block') { const el = typeof element === 'string' ? document.getElementById(element) : element; if (!el) return; const durations = { 'fast': 150, 'normal': 300, 'slow': 600 }; const duration = durations[speed] || durations.normal; el.style.transition = `opacity ${duration}ms ease-in-out`; if (show) { el.style.display = displayType; el.style.opacity = '0'; setTimeout(() => { el.style.opacity = '1'; }, 10); } else { el.style.opacity = '0'; setTimeout(() => { el.style.display = 'none'; el.style.opacity = '1'; }, duration); } } /** * Centralized progress manager for all operations (scanning, relocking) * Fully self-contained with complete ownership of progress elements * Manages both spinner and progress bar with optimized performance */ const ProgressManager = { containerWidth: null, spinnerElement: null, percentageLoaderElement: null, scanCounterElement: null, /** * Internal method to show/hide progress elements * Handles both spinner and progress bar with percentage-based width * @param {boolean} show - true for spinning icon (in progress), false for completion icon * @param {number|null} progressPercent - Width for percentage loader as percentage (0-100) */ _toggleElements(show = false, progressPercent = null) { if (this.spinnerElement) { this.spinnerElement.style.display = 'inline-block'; if (show) { // Show spinning icon (operation in progress) this.spinnerElement.className = 'fa fa-spinner fa-spin rl-spinner'; } else { // Show completion icon (operation complete) this.spinnerElement.className = 'fa fa-check-circle rl-spinner-complete'; } } if (this.percentageLoaderElement) { if (show) { // Show progress bar only during active operations this.percentageLoaderElement.style.display = 'block'; if (progressPercent !== null) { this.percentageLoaderElement.style.width = Math.max(progressPercent, 1) + '%'; } } else { // Hide progress bar when complete this.percentageLoaderElement.style.display = 'none'; } } }, /** * Initialize progress manager - fully self-contained initialization * Creates and caches all required DOM elements and container width * @param {HTMLElement} relockContent - The main content container to append progress elements to */ init(relockContent = null) { // Cache container width for performance const container = document.getElementById('sidepanel-relockTab') || relockContent; this.containerWidth = container ? container.offsetWidth : 300; // Find the button-progress container for progress bar only const buttonProgressContainer = relockContent ? relockContent.querySelector('.rl-button-progress-container') : null; // Create a wrapper for progress elements (progress bar only) let progressWrapper = buttonProgressContainer ? buttonProgressContainer.querySelector('.rl-progress-elements') : null; if (!progressWrapper && buttonProgressContainer) { progressWrapper = document.createElement('div'); progressWrapper.className = 'rl-progress-elements'; buttonProgressContainer.appendChild(progressWrapper); } // Find the scan counter container for spinner placement const scanCounterContainer = relockContent ? relockContent.querySelector('.rl-scan-counter-container') : null; // Create spinner element if not already done - place it in scan counter container if (!this.spinnerElement) { this.spinnerElement = document.createElement('div'); this.spinnerElement.className = 'fa fa-check-circle rl-spinner-complete'; // Start with completion icon // Insert spinner before the scan counter label if container available if (scanCounterContainer) { // Insert as first child (before scan counter label) scanCounterContainer.insertBefore(this.spinnerElement, scanCounterContainer.firstChild); } else if (relockContent) { relockContent.appendChild(this.spinnerElement); } } // Create progress bar element if not already done - place it in progress wrapper if (!this.percentageLoaderElement) { this.percentageLoaderElement = document.createElement('div'); this.percentageLoaderElement.className = 'rl-progress-bar'; // Append to progress wrapper if available if (progressWrapper) { progressWrapper.appendChild(this.percentageLoaderElement); } else if (relockContent) { relockContent.appendChild(this.percentageLoaderElement); } } console.debug(`${SCRIPT_LOG_PREFIX} ProgressManager initialized with container width:`, this.containerWidth); }, /** * Start progress tracking for any operation * @param {string} operationType - 'scan' or 'relock' for logging */ start(operationType = 'operation') { if (!this.containerWidth || !this.spinnerElement || !this.percentageLoaderElement) { this.init(); } this._toggleElements(true, 1); // Show spinning icon with minimal progress width (1%) // Reset and show scan counter for scanning operations if (operationType === 'scan') { this.updateScanCounter(0); } console.debug(`${SCRIPT_LOG_PREFIX} Started ${operationType} with progress tracking`); }, /** * Update progress bar based on current/total items * @param {number} current - Current item count * @param {number} total - Total item count * @param {number} throttle - Update every N items for performance (default: 5) */ update(current, total, throttle = 5) { // Throttle updates for performance - only update every N items if (current % throttle !== 0 && current !== total) return; if (total <= 0) return; const progress = Math.min((current / total) * 100, 100); // Update scan counter this.updateScanCounter(current); //console.debug(`${SCRIPT_LOG_PREFIX} Progress update: ${current}/${total} (${progress.toFixed(1)}%)`); this._toggleElements(true, progress); // Show spinning icon with current progress }, /** * Update scan counter display * @param {number} count - Number of elements scanned */ updateScanCounter(count) { if (this.scanCounterElement) { this.scanCounterElement.textContent = `Elements scanned: ${count}`; } }, /** * Complete progress tracking and show completion icon */ complete() { // Show completion icon (progress bar gets hidden automatically) this._toggleElements(false); }, /** * Reset progress manager state (useful for cleanup) */ reset() { this.containerWidth = null; this.spinnerElement = null; this.percentageLoaderElement = null; this.scanCounterElement = null; } }; function Relock_bootstrap() { const initializeSDK = async () => { try { wmeSDK = getWmeSdk({ scriptId: SCRIPT_ID, scriptName: GM_info.script.name }); const requiredComponents = [ 'DataModel', 'Events', 'State', 'Map' ]; for (const component of requiredComponents) { if (!wmeSDK[component]) { throw new Error(`Required SDK component ${component} not available`); } } // Wait for WME to be fully ready await wmeSDK.Events.once({ eventName: "wme-ready" }); if (!wmeSDK.State.isLoggedIn()) { throw new Error('User not logged in'); } // Cache user level for efficient access during scanning const userInfo = wmeSDK.State.getUserInfo(); if (!userInfo) { throw new Error('Unable to get user info'); } userLevel = userInfo.rank + 1; console.debug(`${SCRIPT_LOG_PREFIX} Cached user level:`, userLevel); await wmeSDK.Events.once({ eventName: "wme-map-data-loaded" }); await Relock_init(); } catch (error) { ErrorHandler.handle(error, 'SDK Initialization', ErrorHandler.SEVERITY.CRITICAL); } }; const waitForSDK = async () => { try { if (unsafeWindow.SDK_INITIALIZED) { await unsafeWindow.SDK_INITIALIZED; await initializeSDK(); } else { await delay(500); waitForSDK(); } } catch (err) { ErrorHandler.handle(err, 'SDK Promise', ErrorHandler.SEVERITY.CRITICAL); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', waitForSDK); } else { waitForSDK(); } } async function Relock_init() { try { if (!wmeSDK) { throw new Error('SDK not initialized'); } const rlStyle = [ '.tg { border-collapse: collapse; border-spacing: 0; margin: 0px auto; }', '.tg td { border-color: black; border-style: solid; border-width: 1px; overflow: hidden; padding: 2px 2px; word-break: normal; }', '.tg .tg-value { text-align: center; vertical-align: top }', '.tg .tg-header { background-color: #ecf4ff; border-color: #000000; font-weight: bold; text-align: center; vertical-align: top }', '.tg .tg-type { text-align: left; vertical-align: top; max-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', '.tg-row:hover { background-color: #f5f5f5; }', '.tg-row.active { background-color: #e8f0fe; }', '.rl-spinner { opacity: 0.8; width: 16px; height: 16px; vertical-align: text-top; display: none; font-size: 14px; }', '.rl-spinner-complete { opacity: 0.8; width: 16px; height: 16px; vertical-align: text-top; font-size: 14px; color: green; }', '.rl-scan-counter-container { display: flex; align-items: center; color: #666; margin-bottom: 5px; margin-right: 5px; gap: 3px; }', '.fa-spin { animation: fa-spin 0.5s infinite linear; }', '@keyframes fa-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }', '.rl-progress-bar { transition: width 0.3s ease-in-out; width: 1%; height: 10px; background-color: green; margin-top: 0; border: 1px solid #333333; display: none; }', '.rl-flex-row { display: flex; align-items: center; margin-bottom: 0; padding: 0; }', '.rl-flex-row > div { flex: 1 1 auto; }', '.rl-flex-row input[type="checkbox"] { margin-right: 6px; vertical-align: middle; }', '.rl-flex-row label { margin-bottom: 0; font-weight: normal; max-width: 230px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; overflow: hidden; vertical-align: middle; }', '.rl-flex-row .rl-flex-right { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }', '.rl-flex-row .rl-flex-counter { font-size: 100%; font-weight: bold; }', '.rl-label { font-size: 95%; margin-left: 5px; vertical-align: middle; }', '.rl-container { margin-right: 5px; margin-bottom: 5px; }', '.rl-info-box { font-size: 85%; padding: 15px; border: 1px solid red; border-radius: 5px; position: relative; margin-right: 5px; }', '.rl-alert-box { border: 1px solid #EBCCD1; background-color: #F2DEDE; color: #AC4947; font-weight: bold; font-size: 90%; border-radius: 5px; padding: 10px; margin: 5px 5px; display: none; }', '.rl-close-btn { cursor: pointer; width: 16px; height: 16px; position: absolute; right: 3px; top: 3px; }', '.rl-lock-icon { cursor: pointer; color: red; }', '.rl-rules-table { font-size: 12px; }', '.rl-lock-status-ok { color: green; }', '.rl-lock-status-error { color: red; }', '.rl-button-progress-container { display: flex; align-items: center; gap: 10px; margin: 10px 5px 10px 0; }', '.rl-progress-elements { display: flex; align-items: center; gap: 5px; flex: 1; }', '.rl-scan-counter { color: #666; margin-bottom: 5px; }', '.rl-info-paragraph-first { margin-bottom: 10px; }', '.rl-info-paragraph-last { margin-bottom: 0; }', '.rl-hide-button { cursor: pointer; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 12px; margin-left: auto; margin-bottom: 8px; }', '.rl-hide-button:hover { color: #333; }', '.rl-version-container { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; margin-right: 5px; }', '.rl-toggle-all-container { display: flex; align-items: center; margin-bottom: 8px; margin-right: 5px; padding: 5px 0; border-bottom: 1px solid #ddd; gap: 8px; }', '.rl-toggle-all-icon { cursor: pointer; font-size: 16px; color: #666; transition: color 0.2s ease; }', '.rl-toggle-all-icon:hover { color: #333; }', '.rl-toggle-all-icon.all-checked { color: #4CAF50; }', '.rl-toggle-all-icon.some-checked { color: #FF9800; }', '.rl-section-title { font-size: 85%; font-weight: bold; }', '.rl-title-version-container { display: flex; flex-direction: column; margin-bottom: 5px; margin-right: 5px; padding: 8px 12px; border-radius: 6px; position: relative; overflow: hidden; }', '.rl-title-version-container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, rgba(0, 87, 183, 0.15) 0%, rgba(0, 87, 183, 0.7) 50%, rgba(255, 215, 0, 0.7) 50%, rgba(255, 215, 0, 0.15) 100%); z-index: -1; }', '.rl-title-version-container .rl-title-content { display: flex; align-items: center; margin-bottom: 4px; }', '.rl-title-version-container .rl-version-content { display: flex; align-items: center; justify-content: space-between; gap: 8px; }', ]; try { GM_addStyle(rlStyle.join('\n')); } catch (styleError) { ErrorHandler.handle(styleError, 'Style Injection', ErrorHandler.SEVERITY.WARNING); const style = document.createElement('style'); style.textContent = rlStyle.join('\n'); document.head.appendChild(style); } } catch (error) { ErrorHandler.handle(error, 'Main Initialization', ErrorHandler.SEVERITY.CRITICAL); return; } // Get road types dynamically from SDK instead of hardcoded mapping let roadTypeConfig = {}; let rulesDB = {}; const relockObject = {}; // Scan throttling variables let scanTimeout; const SCAN_DEBOUNCE_DELAY = 300; // 300ms delay after map movement stops // Current map extent for scan session let currentMapExtent = null; /** * Initialize road types from SDK * @returns {Object} Road types object with structure: {id: {typeName, scan, sdkType}} */ function initializeRoadTypes() { return ErrorHandler.wrapSync(() => { const roadTypes = wmeSDK.DataModel.Segments.getRoadTypes(); const streetTypesMap = {}; // Add all road types from SDK roadTypes.forEach(roadType => { streetTypesMap[roadType.id] = { typeName: roadType.localizedName, scan: true, sdkType: roadType.name }; }); // Add special type for POIs (not in SDK road types) streetTypesMap[POI_ID] = { typeName: POI_NAME, scan: true, sdkType: POI_NAME }; console.log(`${SCRIPT_LOG_PREFIX} Initialized`, Object.keys(streetTypesMap).length, 'road types from SDK'); return streetTypesMap; }, 'Road Types Initialization', ErrorHandler.SEVERITY.CRITICAL)(); // Critical error if this fails } /** * Get typeName by sdkType from roadTypeConfig * @param {string} sdkType - The SDK type to look up * @returns {string} The typeName if found, empty string otherwise */ function getTypeNameBySdkType(sdkType) { return ErrorHandler.wrapSync(() => { const roadType = Object.values(roadTypeConfig).find(rt => rt.sdkType === sdkType); return roadType ? roadType.typeName : ""; }, 'Type Name Lookup', ErrorHandler.SEVERITY.WARNING)(); } function onScreen(obj) { if (!obj || !obj.geometry) return false; return ErrorHandler.wrapSync(() => { // Use current map extent set at scan start for performance if (!currentMapExtent) return false; const [left, bottom, right, top] = currentMapExtent; const geometry = obj.geometry; if (geometry.type === 'Point') { const [lon, lat] = geometry.coordinates; return lon >= left && lon <= right && lat >= bottom && lat <= top; } else if (geometry.type === 'LineString') { return geometry.coordinates.some(([lon, lat]) => lon >= left && lon <= right && lat >= bottom && lat <= top ); } else if (geometry.type === 'Polygon') { // Check exterior ring (first array) and interior rings (if any) return geometry.coordinates.some(ring => ring.some(([lon, lat]) => lon >= left && lon <= right && lat >= bottom && lat <= top ) ); } return true; }, 'Viewport Visibility Check', ErrorHandler.SEVERITY.WARNING)(); } function isVenueEditable(venue) { if (!venue || !venue.id) return false; return ErrorHandler.wrapSync(() => { // Check if venue has pending update requests if (venue.venueUpdateRequests && venue.venueUpdateRequests.length > 0) { return false; } return !venue.isAdLocked && wmeSDK.DataModel.Venues.hasPermissions({ permission: "EDIT_GEOMETRY", venueId: venue.id }); }, 'Venue Editability Check', ErrorHandler.SEVERITY.WARNING)(); } function isSegmentEditable(segment) { if (!segment || !segment.id) return false; return ErrorHandler.wrapSync(() => { return !segment.hasClosures && wmeSDK.DataModel.Segments.hasPermissions({ permission: "EDIT_PROPERTIES", segmentId: segment.id }); }, 'Segment Editability Check', ErrorHandler.SEVERITY.WARNING)(); } async function updateSegmentLock(segment, newLockRank) { if (!segment || !segment.id) return false; try { if (!wmeSDK.DataModel.Segments.hasPermissions({ permission: "EDIT_PROPERTIES", segmentId: segment.id })) { ErrorHandler.handle(`No permission to edit segment: ${segment.id}`, 'Segment Permission Check', ErrorHandler.SEVERITY.WARNING); return false; } await wmeSDK.DataModel.Segments.updateSegment({ segmentId: segment.id, lockRank: newLockRank }); return true; } catch (err) { ErrorHandler.handle(err, 'Segment Lock Update', ErrorHandler.SEVERITY.ERROR); return false; } } async function updateVenueLock(venue, newLockRank) { if (!venue || !venue.id) return false; try { if (!wmeSDK.DataModel.Venues.hasPermissions({ permission: "EDIT_GEOMETRY", venueId: venue.id })) { ErrorHandler.handle(`No permission to edit venue: ${venue.id}`, 'Venue Permission Check', ErrorHandler.SEVERITY.WARNING); return false; } await wmeSDK.DataModel.Venues.updateVenue({ venueId: venue.id, lockRank: newLockRank }); return true; } catch (err) { ErrorHandler.handle(err, 'Venue Lock Update', ErrorHandler.SEVERITY.ERROR); return false; } } function getRoadType(segment) { if (!segment || !segment.roadType) return null; return ErrorHandler.wrapSync(() => { if (localStorage.getItem(ID_KEYS.RESPECT_ROUTING) === 'true' && segment.routingRoadType) { const routingType = roadTypeConfig[segment.routingRoadType]; if (routingType) return routingType.sdkType; } const roadType = roadTypeConfig[segment.roadType]; return roadType ? roadType.sdkType : null; }, 'Road Type Determination', ErrorHandler.SEVERITY.WARNING)(); } /** * Determines if an object (segment or venue) needs lock level adjustment * @param {Object} obj - The segment or venue object * @param {number} targetLockLevel - The desired lock level * @returns {boolean} True if the object needs relocking */ function needsLockAdjustment(obj, targetLockLevel) { if (!obj || typeof obj.lockRank !== 'number' || typeof targetLockLevel !== 'number') { return false; } return ErrorHandler.wrapSync(() => { // Use cached element instead of repeated getElementById if (!cachedElements.allSegmentsCheckbox) { cachedElements.allSegmentsCheckbox = document.getElementById(ID_KEYS.ALL_SEGMENTS); } const includeAllSegments = cachedElements.allSegmentsCheckbox; const allSegmentsInclude = includeAllSegments && includeAllSegments.checked && userLevel > minimumUserLevelForHigherLocks; // Check if user has permission to modify this lock level if (userLevel <= targetLockLevel) { return false; } // Object needs adjustment if lock is too low OR (if enabled) too high return (obj.lockRank < targetLockLevel) || (obj.lockRank > targetLockLevel && allSegmentsInclude); }, 'Lock Adjustment Check', ErrorHandler.SEVERITY.WARNING)(); } function displayHtmlPage(res) { if (res.responseText.match(/Authorization needed/) || res.responseText.match(/ServiceLogin/)) { ErrorHandler.handle( "Authorization is required for using this script. This is one time action.\nNow you will be redirected to the authorization page, where you'll need to approve request.\nAfter confirmation, please close the page and reload WME.", 'Authorization Required', ErrorHandler.SEVERITY.INFO, true ); } const w = window.open(); w.document.open(); w.document.write(res.responseText); w.document.close(); w.location = res.finalUrl; } async function sendHTTPRequest(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, method: 'GET', timeout: REQUEST_TIMEOUT, onload: function (res) { resolve(res); }, onreadystatechange: function (_res) { }, ontimeout: function (_res) { const error = new Error('Request timeout'); ErrorHandler.handle(error, 'HTTP Request', ErrorHandler.SEVERITY.CRITICAL); reject(error); }, onerror: function (_res) { const error = new Error('Request error'); ErrorHandler.handle(error, 'HTTP Request', ErrorHandler.SEVERITY.CRITICAL); reject(error); } }); }); } function validateHTTPResponse(res) { let result = false; let displayError = true; if (res) { switch (res.status) { case 200: displayError = false; if (res.responseHeaders.match(/content-type:\s*application\/json/i)) { result = true; } else if (res.responseHeaders.match(/content-type:\s*text\/html/i)) { displayHtmlPage(res); } break; default: displayError = false; ErrorHandler.handle(`Unsupported status code: ${res.status}`, 'HTTP Response Validation', ErrorHandler.SEVERITY.CRITICAL, { headers: res.responseHeaders, response: res.responseText }); break; } } else { displayError = false; ErrorHandler.handle('Response is empty', 'HTTP Response Validation', ErrorHandler.SEVERITY.CRITICAL); } if (displayError) { ErrorHandler.handle('Error processing request', 'HTTP Response Validation', ErrorHandler.SEVERITY.CRITICAL, { response: res.responseText }); } return result; } async function getAllLockRules() { try { const url = `https://script.google.com/macros/s/${RULES_HASH}/exec?func=getAllLockRulesV2`; const response = await sendHTTPRequest(url); if (!validateHTTPResponse(response)) { throw new Error('Invalid response received'); } const data = JSON.parse(response.responseText); if (data.result !== "success") { throw new Error('Failed to get locking rules: ' + (data.error || 'Unknown error')); } await initUI(data.rules); } catch (err) { ErrorHandler.handle(err, 'Lock Rules Fetching', ErrorHandler.SEVERITY.CRITICAL); } } const scanArea = ErrorHandler.wrapAsync(async (showLoader = true) => { try { // Get current map extent once at scan start currentMapExtent = wmeSDK.Map.getMapExtent(); let topCountry = wmeSDK.DataModel.Countries.getTopCountry(); if (!topCountry || !topCountry.abbr) { // FIXME: there is a strange behavior (bug?) when retrieving the top country - // sometimes DataModel.Countries returns an empty object and so getTopCountry() returns null // as a temporary workaround, we will go old way and use global W object to get the countries. const countries = W?.model?.countries?.getObjectArray(); if (countries?.[0]?.attributes) { topCountry = countries[0].attributes; } else { ErrorHandler.handle('Top country not found or invalid', 'Country Retrieval', ErrorHandler.SEVERITY.ERROR); return; } } // Start progress tracking for scanning if (showLoader) { ProgressManager.start('scan'); } hideInactiveCities(); // Clear all relock arrays for fresh scan Object.keys(relockObject).forEach(key => { relockObject[key].length = 0; }); let foundBadlocks = false; const respectRoutingRoadType = cachedElements.respectRoutingElement && cachedElements.respectRoutingElement.checked; let count = 0; const ABBR = rulesDB[topCountry.abbr] ? rulesDB[topCountry.abbr][0].Locks : DEFAULT_STREET_LOCKS; console.debug(`${SCRIPT_LOG_PREFIX} Rules to be used`, ABBR); const segments = wmeSDK.DataModel.Segments.getAll(); const venues = wmeSDK.DataModel.Venues.getAll(); // Pre-filter on-screen segments and venues for accurate progress tracking const onScreenSegments = segments.filter(segment => onScreen(segment)); const onScreenVenues = venues.filter(venue => onScreen(venue)); // Calculate total items for progress tracking (limited by scan limit) const totalOnScreenItems = onScreenSegments.length + onScreenVenues.length; let processedItems = 0; for (const segment of onScreenSegments) { try { if (count >= FIX_LIMIT_COUNT) break; processedItems++; if (showLoader) { ProgressManager.update(processedItems, totalOnScreenItems, 10); // Update every 10 items } const roadType = getRoadType(segment); if (!roadType) continue; if (!isSegmentEditable(segment)) continue; const effectiveRoadType = respectRoutingRoadType && segment.routingRoadType ? segment.routingRoadType : segment.roadType; const curStreet = roadTypeConfig[effectiveRoadType]; if (!curStreet || !curStreet.scan) continue; let cityID = null; try { if (segment.primaryStreetId) { const street = wmeSDK.DataModel.Streets.getById({ streetId: segment.primaryStreetId }); cityID = street ? street.cityId : null; } } catch { console.warn(`${SCRIPT_LOG_PREFIX} Could not get street info for segment:`, segment.id); } const cityRules = cityID && rulesDB[topCountry.abbr] && rulesDB[topCountry.abbr][cityID]; const stLocks = cityRules ? cityRules.Locks : ABBR; const desiredLockLevel = stLocks[curStreet.sdkType] - 1; // Check if segment needs lock adjustment using centralized logic if (needsLockAdjustment(segment, desiredLockLevel)) { if (!relockObject[curStreet.sdkType]) { relockObject[curStreet.sdkType] = []; } relockObject[curStreet.sdkType].push({ object: segment, lockRank: desiredLockLevel }); foundBadlocks = true; count++; // increment only if a bad lock is found } } catch (segmentError) { console.error(`${SCRIPT_LOG_PREFIX} Error processing segment:`, segmentError); continue; } } if (roadTypeConfig[POI_ID] && roadTypeConfig[POI_ID].scan) { onScreenVenues.forEach(venue => { if (!isVenueEditable(venue)) return; if (count >= FIX_LIMIT_COUNT) return; processedItems++; if (showLoader) { ProgressManager.update(processedItems, totalOnScreenItems, 10); // Update every 10 items } const address = wmeSDK.DataModel.Venues.getAddress({ venueId: venue.id }); const cityID = address && address.street ? address.street.cityId : null; let desiredLockLevel = (cityID && rulesDB[topCountry.abbr] && rulesDB[topCountry.abbr][cityID]) ? rulesDB[topCountry.abbr][cityID].Locks[POI_NAME] : ABBR[POI_NAME]; desiredLockLevel--; // Check if venue needs lock adjustment using centralized logic if (needsLockAdjustment(venue, desiredLockLevel)) { if (!relockObject[POI_NAME]) { relockObject[POI_NAME] = []; } relockObject[POI_NAME].push({ object: venue, lockRank: desiredLockLevel }); foundBadlocks = true; count++; // increment only if a bad lock is found } }); } Object.entries(relockObject).forEach(([key, value]) => { const idPrefix = ID_KEYS.ELM_PREFIX + key + ID_KEYS.ROAD_TYPE_CONTAINER_SUFFIX; // Use cached container instead of repeated getElementById if (!cachedElements.roadTypeContainers[key]) { cachedElements.roadTypeContainers[key] = document.getElementById(idPrefix); } const rightParentElement = cachedElements.roadTypeContainers[key]; if (!rightParentElement) return; // Find existing elements or create them if they don't exist let lockIconElement = rightParentElement.querySelector('.fa.fa-lock'); let counterElement = rightParentElement.querySelector('.rl-flex-counter'); if (!counterElement) { counterElement = document.createElement('div'); counterElement.className = 'rl-flex-counter'; rightParentElement.appendChild(counterElement); } if (value.length !== 0) { counterElement.textContent = value.length; if (!lockIconElement) { lockIconElement = document.createElement('div'); lockIconElement.className = 'fa fa-lock rl-lock-icon'; lockIconElement.onclick = (function(roadTypeKey) { return function() { relock(relockObject, roadTypeKey); }; })(key); rightParentElement.insertBefore(lockIconElement, counterElement); } } else { counterElement.textContent = '-'; if (lockIconElement) { lockIconElement.remove(); } } }); // Update relock button state if (foundBadlocks) { cachedElements.relockAllbutton.removeAttribute('disabled'); updateLockStatusIcon(true); } else { cachedElements.relockAllbutton.setAttribute('disabled', true); updateLockStatusIcon(false); } // Complete progress tracking for scanning if (showLoader) { ProgressManager.complete(); } } catch (error) { ErrorHandler.handle(error, 'Area Scanning', ErrorHandler.SEVERITY.WARNING); // Complete progress tracking if an error occurs during scanning if (showLoader) { ProgressManager.complete(); } } }, 'Area Scanning', ErrorHandler.SEVERITY.WARNING); /** * Debounced scan function - only scans after map movement has settled * Prevents excessive scanning during rapid map movements by waiting for * a pause in map events before executing the scan. * @returns {void} */ const debouncedScan = () => { clearTimeout(scanTimeout); scanTimeout = setTimeout(() => { scanArea(); }, SCAN_DEBOUNCE_DELAY); }; /** * Update lock status icon with appropriate CSS class * Uses cached DOM element for better performance * @param {boolean} hasErrors - Whether there are lock errors */ function updateLockStatusIcon(hasErrors) { if (!cachedElements.lockColorElement) return; cachedElements.lockColorElement.className = hasErrors ? 'fa fa-lock rl-lock-status-error' : 'fa fa-lock rl-lock-status-ok'; } async function initUI(rules) { rulesDB = rules; roadTypeConfig = initializeRoadTypes(); const { tabLabel, tabPane } = await wmeSDK.Sidebar.registerScriptTab(); const relockContent = document.createElement('div'); const relockTitle = document.createElement('wz-overline'); const rulesSubTitle = document.createElement('wz-h7'); const relockAllbutton = document.createElement('wz-button'); const infoBox = document.createElement('p'); const versionTitle = document.createElement('wz-label'); const resultsCntr = document.createElement('div'); const rulesCntr = document.createElement('div'); const alertCntr = document.createElement('div'); const infoToggleButton = document.createElement('div'); const inputDiv1 = document.createElement('div'); const inputDiv2 = document.createElement('div'); const includeAllSegments = document.createElement('input'); const includeAllSegmentsLabel = document.createElement('label'); const respectRouting = document.createElement('input'); const respectRoutingLabel = document.createElement('label'); const relockTabLabel = document.createTextNode('Re-lock Segments & POI'); const lockStatusIcon = document.createElement('span'); const scanCounterLabel = document.createElement('div'); // Removed unnecessary IDs - these elements are referenced directly lockStatusIcon.className = 'fa fa-lock rl-lock-status-ok'; tabLabel.textContent = "Re-"; tabLabel.appendChild(lockStatusIcon); relockTitle.appendChild(relockTabLabel); // Create description content using proper paragraph elements const paragraph1 = document.createElement('p'); paragraph1.textContent = 'Your on-screen area is automatically scanned when you load or pan around. Pressing the lock behind each type will relock only those results, or you can choose to relock all.'; paragraph1.className = 'rl-info-paragraph-first'; const paragraph2 = document.createElement('p'); paragraph2.textContent = 'You can only relock segments lower or equal to your current editor level. Segments locked higher than normal are left alone.'; paragraph2.className = 'rl-info-paragraph-last'; infoBox.appendChild(paragraph1); infoBox.appendChild(paragraph2); infoBox.className = 'rl-info-box'; // Use consistent ID naming pattern from ID_KEYS infoBox.id = ID_KEYS.INFO_BOX; infoToggleButton.className = 'fa fa-question-circle rl-hide-button'; infoToggleButton.title = 'How it works?'; infoToggleButton.onclick = () => { // Use cached element instead of repeated getElementById if (!cachedElements.infoBox) { cachedElements.infoBox = document.getElementById(ID_KEYS.INFO_BOX); } const infoBoxElement = cachedElements.infoBox; const isHidden = infoBoxElement.style.display === 'none'; if (isHidden) { animateElement(infoBoxElement, true, 'fast'); localStorage.setItem(ID_KEYS.MSG_HIDE, '0'); } else { animateElement(infoBoxElement, false, 'fast'); localStorage.setItem(ID_KEYS.MSG_HIDE, '1'); } }; rulesSubTitle.textContent = 'Active rules'; versionTitle.textContent = 'Version ' + SCRIPT_VERSION; // Configure scan counter - removed unnecessary ID since element is referenced directly scanCounterLabel.textContent = 'Elements scanned: 0'; scanCounterLabel.className = 'message'; // Create container for scan counter with spinner const scanCounterContainer = document.createElement('div'); scanCounterContainer.className = 'rl-scan-counter-container'; scanCounterContainer.appendChild(scanCounterLabel); // Cache the scan counter element for ProgressManager cachedElements.scanCounterElement = scanCounterLabel; ProgressManager.scanCounterElement = scanCounterLabel; relockAllbutton.color = 'primary'; relockAllbutton.textContent = 'Relock All'; relockAllbutton.size = 'md'; // Removed unnecessary ID - button is referenced directly through cached variable relockAllbutton.onclick = () => { relockAll(); }; cachedElements.relockAllbutton = relockAllbutton; cachedElements.lockColorElement = lockStatusIcon; includeAllSegments.type = 'checkbox'; includeAllSegments.name = "name"; includeAllSegments.value = "value"; includeAllSegments.checked = (localStorage.getItem(ID_KEYS.ALL_SEGMENTS) == 'true'); includeAllSegments.id = ID_KEYS.ALL_SEGMENTS; includeAllSegments.onclick = () => { localStorage.setItem(ID_KEYS.ALL_SEGMENTS, includeAllSegments.checked.toString()); scanArea(); // No parameters needed relockShowAlert(); }; includeAllSegmentsLabel.htmlFor = ID_KEYS.ALL_SEGMENTS; includeAllSegmentsLabel.textContent = 'Also relock higher locked objects'; includeAllSegmentsLabel.className = 'rl-label'; // Respect routing road type respectRouting.type = 'checkbox'; respectRouting.name = "name"; respectRouting.value = "value"; respectRouting.checked = (localStorage.getItem(ID_KEYS.RESPECT_ROUTING) == 'true'); respectRouting.id = ID_KEYS.RESPECT_ROUTING; // Cache the element for performance optimization cachedElements.respectRoutingElement = respectRouting; respectRouting.onclick = () => { localStorage.setItem(ID_KEYS.RESPECT_ROUTING, respectRouting.checked.toString()); scanArea(); // No parameters needed }; respectRoutingLabel.htmlFor = ID_KEYS.RESPECT_ROUTING; respectRoutingLabel.textContent = 'Respect routing road type'; respectRoutingLabel.className = 'rl-label'; resultsCntr.className = 'rl-container'; // Create toggle all checkboxes container and icon const toggleAllContainer = document.createElement('div'); toggleAllContainer.className = 'rl-toggle-all-container'; const toggleAllIcon = document.createElement('i'); toggleAllIcon.className = 'fa fa-check-square rl-toggle-all-icon'; toggleAllIcon.title = 'Toggle all road type checkboxes'; const sectionTitle = document.createElement('span'); sectionTitle.textContent = 'Bad locks found (limited to ' + FIX_LIMIT_COUNT + ' per pass):'; sectionTitle.className = 'rl-section-title'; // Cache the toggle icon for performance cachedElements.toggleAllCheckboxesIcon = toggleAllIcon; /** * Updates the toggle icon appearance based on checkbox states */ function updateToggleIconState() { const checkboxes = Object.values(cachedElements.checkboxElements); const checkedCount = checkboxes.filter(cb => cb && cb.checked).length; const totalCount = checkboxes.length; if (checkedCount === 0) { toggleAllIcon.className = 'fa fa-square-o rl-toggle-all-icon'; toggleAllIcon.title = 'Select all road types'; } else if (checkedCount === totalCount) { toggleAllIcon.className = 'fa fa-check-square rl-toggle-all-icon all-checked'; toggleAllIcon.title = 'Unselect all road types'; } else { toggleAllIcon.className = 'fa fa-minus-square rl-toggle-all-icon some-checked'; toggleAllIcon.title = 'Select all road types'; } } /** * Toggles all road type checkboxes */ function toggleAllCheckboxes() { return ErrorHandler.wrapSync(() => { const checkboxes = Object.values(cachedElements.checkboxElements); const checkedCount = checkboxes.filter(cb => cb && cb.checked).length; const shouldCheck = checkedCount === 0; // Toggle all checkboxes Object.entries(cachedElements.checkboxElements).forEach(([sdkType, checkbox]) => { if (checkbox) { checkbox.checked = shouldCheck; // Update localStorage and roadTypeConfig const idPrefix = ID_KEYS.ELM_PREFIX + sdkType; const storageKey = idPrefix + ID_KEYS.CHECKBOX_SUFFIX; localStorage.setItem(storageKey, shouldCheck.toString()); // Update scan property in roadTypeConfig const roadTypeEntry = Object.values(roadTypeConfig).find(rt => rt.sdkType === sdkType); if (roadTypeEntry) { roadTypeEntry.scan = shouldCheck; } } }); // Update toggle icon state updateToggleIconState(); // Trigger rescan scanArea(); }, 'Toggle All Checkboxes', ErrorHandler.SEVERITY.WARNING)(); } toggleAllIcon.onclick = toggleAllCheckboxes; toggleAllContainer.appendChild(toggleAllIcon); toggleAllContainer.appendChild(sectionTitle); resultsCntr.appendChild(toggleAllContainer); Object.entries(roadTypeConfig).forEach(([key, value]) => { const rowContainer = document.createElement('div'); rowContainer.className = 'rl-flex-row'; const leftKeyContainer = document.createElement('div'); const checkboxElement = document.createElement('input'); const labelElement = document.createElement('label'); const idPrefix = ID_KEYS.ELM_PREFIX + value.sdkType; checkboxElement.type = 'checkbox'; checkboxElement.checked = (localStorage.getItem(idPrefix + ID_KEYS.CHECKBOX_SUFFIX) == 'true'); checkboxElement.id = idPrefix + ID_KEYS.CHECKBOX_SUFFIX; // Initialize scan property from checkbox state value.scan = checkboxElement.checked; checkboxElement.onclick = (function(roadTypeKey, storageKey) { return function() { localStorage.setItem(storageKey, checkboxElement.checked.toString()); // Update scan property directly for immediate use roadTypeConfig[roadTypeKey].scan = checkboxElement.checked; // Update toggle icon state when individual checkbox changes updateToggleIconState(); scanArea(); }; })(key, idPrefix + ID_KEYS.CHECKBOX_SUFFIX); labelElement.htmlFor = idPrefix + ID_KEYS.CHECKBOX_SUFFIX; // Cache checkbox element for performance cachedElements.checkboxElements[value.sdkType] = checkboxElement; labelElement.textContent = value.typeName; labelElement.title = value.typeName; leftKeyContainer.appendChild(checkboxElement); leftKeyContainer.appendChild(labelElement); const rightParentElement = document.createElement('div'); rightParentElement.className = 'rl-flex-right'; const counterElement = document.createElement('div'); counterElement.className = 'rl-flex-counter'; counterElement.textContent = '-'; rightParentElement.id = idPrefix + ID_KEYS.ROAD_TYPE_CONTAINER_SUFFIX; rightParentElement.appendChild(counterElement); rowContainer.appendChild(leftKeyContainer); rowContainer.appendChild(rightParentElement); resultsCntr.appendChild(rowContainer); }); alertCntr.className = 'rl-alert-box'; alertCntr.textContent = 'Watch out for map exceptions, some higher locks are there for a reason!'; // Cache alert container for performance cachedElements.alertContainer = alertCntr; let rowElm; let colElm; const tableElm = document.createElement('table'); tableElm.className = 'tg'; const bodyElm = document.createElement('tbody'); let countryRules = null; const topCountry = wmeSDK.DataModel.Countries.getTopCountry(); if (topCountry && topCountry.abbr) { countryRules = rulesDB[topCountry.abbr]; } else { ErrorHandler.handle('Top country not found or invalid', 'Country Retrieval in initUI', ErrorHandler.SEVERITY.ERROR); } if (countryRules) { Object.entries(countryRules).forEach(([key, value]) => { if (key == "CountryName") return; rowElm = document.createElement('tr'); rowElm.className = "tg-row"; rowElm.dataset.name = parseInt(key) === 0 ? 'country' : value.CityName; colElm = document.createElement('td'); colElm.className = "tg-header"; const cityName = parseInt(key) === 0 ? countryRules.CountryName : value.CityName; colElm.textContent = cityName; colElm.colSpan = 6; rowElm.appendChild(colElm); tableElm.appendChild(rowElm); const maxCol = 2; let colIndex = 0; rowElm = document.createElement('tr'); Object.entries(value.Locks).forEach(([k, v]) => { if (v) { rowElm.className = "tg-row"; rowElm.dataset.name = parseInt(key) === 0 ? 'country' : value.CityName; // need to hard code 'country' to identify later if (colIndex < maxCol) { colElm = document.createElement('td'); colElm.className = "tg-type"; const streetType = getTypeNameBySdkType(k) || k; colElm.textContent = streetType; colElm.title = streetType; rowElm.appendChild(colElm); colElm = document.createElement('td'); colElm.className = "tg-value"; colElm.textContent = v; rowElm.appendChild(colElm); colIndex++; if (colIndex == maxCol) { colIndex = 0; tableElm.appendChild(rowElm); rowElm = document.createElement('tr'); } } } }); tableElm.appendChild(rowElm); }); } tableElm.appendChild(bodyElm); rulesCntr.className = 'rl-rules-table'; rulesCntr.appendChild(tableElm); // add to stage // Create title and version container const titleVersionContainer = document.createElement('div'); titleVersionContainer.className = 'rl-title-version-container'; // Create content wrapper for title const titleContent = document.createElement('div'); titleContent.className = 'rl-title-content'; titleContent.appendChild(relockTitle); // Create content wrapper for version and hide button const versionContent = document.createElement('div'); versionContent.className = 'rl-version-content'; versionContent.appendChild(versionTitle); versionContent.appendChild(infoToggleButton); titleVersionContainer.appendChild(titleContent); titleVersionContainer.appendChild(versionContent); relockContent.appendChild(titleVersionContainer); // Always append the info box relockContent.appendChild(infoBox); // Set initial visibility based on localStorage if (localStorage.getItem(ID_KEYS.MSG_HIDE) === '1') { infoBox.style.display = 'none'; } inputDiv1.appendChild(respectRouting); inputDiv1.appendChild(respectRoutingLabel); inputDiv2.appendChild(includeAllSegments); inputDiv2.appendChild(includeAllSegmentsLabel); // Hide the "Also relock higher locked objects" option if user level is too low if (userLevel < minimumUserLevelForHigherLocks) { inputDiv2.style.display = 'none'; } relockContent.appendChild(inputDiv1); relockContent.appendChild(inputDiv2); relockContent.appendChild(alertCntr); // Create a flex container for the relock button and progress elements const buttonProgressContainer = document.createElement('div'); buttonProgressContainer.className = 'rl-button-progress-container'; buttonProgressContainer.appendChild(relockAllbutton); relockContent.appendChild(buttonProgressContainer); // Append scan counter container (created earlier) relockContent.appendChild(scanCounterContainer); relockContent.appendChild(resultsCntr); relockContent.appendChild(rulesSubTitle); relockContent.appendChild(rulesCntr); tabPane.appendChild(relockContent); // Initialize ProgressManager after UI elements are in the DOM ProgressManager.init(relockContent); // Set initial toggle icon state after all checkboxes are cached updateToggleIconState(); const eventHandlers = []; function registerEventHandler(eventName, handler) { return ErrorHandler.wrapSync(() => { const subscription = wmeSDK.Events.on({ eventName: eventName, eventHandler: handler }); eventHandlers.push(subscription); return subscription; }, `Event Handler Registration (${eventName})`, ErrorHandler.SEVERITY.ERROR)(); } const scanHandler = () => debouncedScan(); registerEventHandler("wme-map-move-end", scanHandler); registerEventHandler("wme-after-edit", scanHandler); registerEventHandler("wme-after-undo", scanHandler); //registerEventHandler("wme-no-edits", scanHandler); registerEventHandler("wme-map-zoom-changed", scanHandler); function cleanup() { eventHandlers.forEach(handler => { try { handler.remove(); } catch (err) { console.error(`${SCRIPT_LOG_PREFIX} Error removing event handler:`, err); } }); eventHandlers.length = 0; } const cleanupScript = () => { try { cleanup(); const tabElement = document.getElementById('sidepanel-relockTab'); if (tabElement) { tabElement.remove(); } if (window.relockTimer) { clearTimeout(window.relockTimer); } ProgressManager.complete(); console.log(`${SCRIPT_LOG_PREFIX} Cleanup completed successfully`); } catch (error) { console.error(`${SCRIPT_LOG_PREFIX} Error during cleanup:`, error); } }; if (typeof window.addEventListener === 'function') { window.addEventListener('beforeunload', cleanupScript); } relockShowAlert(); scanHandler(); } async function relock(obj, key) { try { const objects = obj[key]; const total = objects.length; // Start progress tracking for individual relock operation ProgressManager.start('relock'); // Process objects individually since SDK doesn't support batch actions for (let i = 0; i < total; i++) { const feature = objects[i]; try { if (key === POI_NAME) { await updateVenueLock(feature.object, feature.lockRank); } else { await updateSegmentLock(feature.object, feature.lockRank); } // Update progress ProgressManager.update(i + 1, total, 1); // Update every item for individual operations // Small delay to prevent overwhelming the system if ((i + 1) % 10 === 0) { await delay(100); } } catch (err) { console.error(`${SCRIPT_LOG_PREFIX} Error updating feature:`, err); continue; } } ProgressManager.complete(); } catch (error) { console.error(`${SCRIPT_LOG_PREFIX} Error in relock operation:`, error); ProgressManager.complete(); } } async function relockAll() { try { // Calculate total items across all types const totalItems = Object.values(relockObject).reduce((sum, objects) => sum + objects.length, 0); // Start progress tracking for bulk relock operation ProgressManager.start('relock-all'); let processedItems = 0; // Process each type of feature (segments, POIs, etc.) for (const [key, objects] of Object.entries(relockObject)) { if (objects.length === 0) continue; // Process objects individually for (const feature of objects) { try { if (key === POI_NAME) { await updateVenueLock(feature.object, feature.lockRank); } else { await updateSegmentLock(feature.object, feature.lockRank); } processedItems++; ProgressManager.update(processedItems, totalItems, 5); // Update every 5 items // Small delay every 10 updates to prevent overwhelming the system if (processedItems % 10 === 0) { await delay(100); } } catch (err) { console.error(`${SCRIPT_LOG_PREFIX} Error updating feature:`, err); continue; } } } await scanArea(false); // Don't show loader during relock operation ProgressManager.complete(); } catch (error) { console.error(`${SCRIPT_LOG_PREFIX} Error in relockAll operation:`, error); ProgressManager.complete(); } } function relockShowAlert() { // Use cached element instead of getElementById if (!cachedElements.allSegmentsCheckbox) { cachedElements.allSegmentsCheckbox = document.getElementById(ID_KEYS.ALL_SEGMENTS); } const includeAllSegments = cachedElements.allSegmentsCheckbox; if (includeAllSegments && includeAllSegments.checked) { animateElement(cachedElements.alertContainer, true, 'fast'); } else { animateElement(cachedElements.alertContainer, false, 'fast'); } } function hideInactiveCities() { const allRows = document.querySelectorAll('tr.tg-row'); allRows.forEach((row) => { let isActive = false; const cities = wmeSDK.DataModel.Cities.getAll(); for (const city of cities) { if (city.name === row.dataset.name) { isActive = true; break; } } if (isActive || row.dataset.name == 'country') { animateElement(row, true, 'fast', 'table-row'); } else { animateElement(row, false, 'fast'); } }); } await getAllLockRules(); } Relock_bootstrap(); })();