您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlights completed WME segments on a separate map layer with a toggle. Repopulates layer on map change.
// ==UserScript== // @name WME Segment Completer // @namespace http://tampermonkey.net/ // @version 2.7.5 // @description Highlights completed WME segments on a separate map layer with a toggle. Repopulates layer on map change. // @author Stephen Wilmot-Doxey (iDroidGuy) // @match https://www.waze.com/editor* // @match https://www.waze.com/*/editor* // @match https://beta.waze.com/editor* // @match https://beta.waze.com/*/editor* // @icon https://www.google.com/s2/favicons?sz=64&domain=waze.com // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.13.2/jquery-ui.min.js // @require https://greasyfork.org/scripts/443221-wazewrap/code/WazeWrap.js?version=1235688 // Included for timing, not switcher add // ==/UserScript== /* globals $, W, WazeWrap, OpenLayers */ /* eslint-disable no-undef, camelcase, prefer-const */ (function() { 'use strict'; // --- Configuration --- const STORAGE_PREFIX = 'WMEComplete_'; const COMPLETED_SEGMENTS_KEY = STORAGE_PREFIX + 'completedSegments'; const COMPLETED_COLOR_KEY = STORAGE_PREFIX + 'completedColor'; const LAYER_VISIBLE_KEY = STORAGE_PREFIX + 'layerVisible'; // Persist visibility state const DEFAULT_COMPLETED_COLOR = '#00FF00'; // Default highlight: bright green const LAYER_NAME = 'Completed Segments'; // Layer name in panel const UNIQUE_LAYER_NAME = '__SegmentCompleterLayer'; // Internal ID for WME const SELECTION_DELAY = 100; // ms delay before processing selection change const REPOPULATE_DELAY = 500; // ms delay before repopulating layer after map move/zoom // --- Globals --- let completedSegments = {}; // { segmentId: true } let completedColor = DEFAULT_COMPLETED_COLOR; let completionLayer = null; // Our custom OpenLayers Vector layer let currentApplicableSegments = []; // Currently selected segment object(s) for the panel checkbox let isEntireStreetSelected = false; // Flag if selection is a whole street let selectionTimeout = null; // Debounce timer for selection changes let repopulateTimeout = null; // Debounce timer for layer repopulation let featureMap = {}; // Map WME segment ID -> OL feature ID on our layer let isLayerInitiallyVisible = true; // Default visibility // --- Styles --- // Basic CSS for the settings panel and toggle button GM_addStyle(` #wme-complete-settings-panel { position: fixed; top: 100px; right: 20px; background-color: white; border: 1px solid #ccc; border-radius: 8px; padding: 15px; z-index: 1001; box-shadow: 0 4px 8px rgba(0,0,0,0.1); font-family: sans-serif; display: none; cursor: move; min-width: 220px; } #wme-complete-settings-panel h3 { margin-top: 0; margin-bottom: 10px; font-size: 16px; border-bottom: 1px solid #eee; padding-bottom: 5px; cursor: move; } #wme-complete-settings-panel label { display: block; margin-bottom: 5px; font-size: 14px; } #wme-complete-color-input, #wme-complete-hex-input { padding: 5px; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 10px; } #wme-complete-color-input { width: 50px; vertical-align: middle; } #wme-complete-hex-input { width: 80px; margin-left: 5px; vertical-align: middle; } #wme-complete-save-button { padding: 5px 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; vertical-align: middle; margin-left: 10px; } #wme-complete-save-button:hover { background-color: #0056b3; } #wme-complete-toggle-button { position: fixed; top: 65px; right: 20px; z-index: 1000; padding: 8px 12px; background-color: #f8f9fa; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #wme-complete-toggle-button:hover { background-color: #e2e6ea; } #wme-complete-checkbox-area, #wme-complete-visibility-area { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; } #wme-complete-checkbox-area label, #wme-complete-visibility-area label { display: flex; align-items: center; cursor: pointer; font-weight: bold; font-size: 14px; } #wme-complete-checkbox-area input[type="checkbox"], #wme-complete-visibility-area input[type="checkbox"] { margin-right: 8px; width: 16px; height: 16px; vertical-align: middle; } #wme-complete-no-segment-msg { font-style: italic; color: #888; font-size: 13px; } `); // --- Initialization --- function initialize() { console.log("WME Segment Completer: Initializing (v2.7.5 - Revert Viewport Opt)..."); loadSettings(); createSettingsPanel(); createToggleButton(); waitForWME(); console.log("WME Segment Completer: Initialized."); } function loadSettings() { // Load data from Tampermonkey storage const savedSegments = GM_getValue(COMPLETED_SEGMENTS_KEY, '{}'); try { completedSegments = JSON.parse(savedSegments); if (typeof completedSegments !== 'object' || completedSegments === null) { completedSegments = {}; } } catch (e) { console.error("WME Segment Completer: Error parsing saved segments.", e); completedSegments = {}; } completedColor = GM_getValue(COMPLETED_COLOR_KEY, DEFAULT_COMPLETED_COLOR); isLayerInitiallyVisible = GM_getValue(LAYER_VISIBLE_KEY, true); // Default to visible console.log(`WME Segment Completer: Loaded ${Object.keys(completedSegments).length} completed segments, color ${completedColor}, visibility ${isLayerInitiallyVisible}`); } function saveSettings(saveVisibility = true) { // Save data to Tampermonkey storage try { GM_setValue(COMPLETED_SEGMENTS_KEY, JSON.stringify(completedSegments)); GM_setValue(COMPLETED_COLOR_KEY, completedColor); // Only save visibility if requested (e.g., not during cleanup in populate) if (saveVisibility && completionLayer) { GM_setValue(LAYER_VISIBLE_KEY, completionLayer.getVisibility()); } } catch (e) { console.error("WME Segment Completer: Error saving settings.", e); } } // --- UI Creation --- function createSettingsPanel() { // Build the floating panel HTML const panel = document.createElement('div'); panel.id = 'wme-complete-settings-panel'; panel.innerHTML = ` <h3>Segment Completer</h3> <div> <label for="wme-complete-color-input" style="display: inline-block; margin-bottom: 10px;">Highlight Color:</label><br> <input type="color" id="wme-complete-color-input" value="${completedColor}"> <input type="text" id="wme-complete-hex-input" value="${completedColor}" size="7" maxlength="7" pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"> <button id="wme-complete-save-button">Save</button> </div> <div id="wme-complete-checkbox-area"> <span id="wme-complete-no-segment-msg">Select a segment or entire street</span> </div> <div id="wme-complete-visibility-area"> </div> `; document.body.appendChild(panel); // Make panel draggable via its header try { $(panel).draggable({ handle: "h3" }); } catch(e) { console.error("WME Segment Completer: Failed to make panel draggable.", e); } // Sync color picker and text input const colorInput = panel.querySelector('#wme-complete-color-input'); const hexInput = panel.querySelector('#wme-complete-hex-input'); colorInput.addEventListener('input', (e) => { hexInput.value = e.target.value; }); hexInput.addEventListener('input', (e) => { if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(e.target.value)) { colorInput.value = e.target.value; } }); // Handle color save button click panel.querySelector('#wme-complete-save-button').addEventListener('click', () => { const newColor = hexInput.value; if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(newColor)) { completedColor = newColor; saveSettings(); // Save new color updateCompletionLayerStyles(); // Apply new color to the layer alert('Color saved!'); } else { alert('Invalid Hex color format.'); } }); updateCheckboxArea(); // Set initial state for the "Mark as Complete" checkbox area } function addVisibilityToggle() { // Adds the "Show Highlights Layer" checkbox to the settings panel const visibilityArea = document.getElementById('wme-complete-visibility-area'); if (!visibilityArea || !completionLayer) { console.error("WME Segment Completer: Cannot add visibility toggle - area or layer missing."); return; } visibilityArea.innerHTML = ''; // Clear potential placeholder const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'wme-complete-visibility-checkbox'; checkbox.checked = completionLayer.getVisibility(); // Sync with current layer state // Toggle layer visibility on change and save the state checkbox.addEventListener('change', () => { completionLayer.setVisibility(checkbox.checked); // If turning layer on, repopulate immediately to show current view if (checkbox.checked) { populateCompletionLayer(); } saveSettings(); // Save the new visibility state }); label.appendChild(checkbox); label.appendChild(document.createTextNode(' Show Highlights Layer')); visibilityArea.appendChild(label); console.log("WME Segment Completer: Added visibility toggle to settings panel."); } function createToggleButton() { // Button to show/hide the settings panel const button = document.createElement('button'); button.id = 'wme-complete-toggle-button'; button.textContent = 'Completer Settings'; button.addEventListener('click', () => { const panel = document.getElementById('wme-complete-settings-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); document.body.appendChild(button); } // --- WME Integration --- function waitForWME() { // Wait until WME essentials and OpenLayers are ready const interval = setInterval(() => { // Check for WME core objects and OpenLayers library const wazeWrapReady = (typeof WazeWrap !== 'undefined' && WazeWrap.Ready); if (typeof W !== 'undefined' && W && W.map && W.model && W.selectionManager && typeof OpenLayers !== 'undefined' && (wazeWrapReady || typeof WazeWrap === 'undefined') ) { clearInterval(interval); console.log("WME Segment Completer: WME Ready. Setting up..."); createCompletionLayer(); setupEventHandlers(); // Setup selection AND map change handlers populateCompletionLayer(); // Initial population addVisibilityToggle(); // Add toggle after layer exists setTimeout(handleSelectionChange, SELECTION_DELAY); // Check initial selection } }, 500); } function createCompletionLayer() { // Create the custom OpenLayers Vector layer for highlights if (!OpenLayers || !W || !W.map) { console.error("WME Segment Completer: OpenLayers or W.map not available to create layer."); return; } completionLayer = new OpenLayers.Layer.Vector(LAYER_NAME, { displayInLayerSwitcher: true, // Let WME try to handle it visibility: isLayerInitiallyVisible, // Use loaded visibility state uniqueName: UNIQUE_LAYER_NAME, styleMap: new OpenLayers.StyleMap({ 'default': getCompletionStyle() }) }); W.map.addLayer(completionLayer); console.log(`WME Segment Completer: Created and added '${LAYER_NAME}' layer. Initial visibility: ${isLayerInitiallyVisible}`); } function setupEventHandlers() { // Listen for selection changes in WME if (W && W.selectionManager && W.selectionManager.events && typeof W.selectionManager.events.register === 'function') { W.selectionManager.events.register("selectionchanged", null, delayedHandleSelectionChange); console.log("WME Segment Completer: Registered selection change handler."); } else { console.error("WME Segment Completer: Could not register selection change handler."); } // Listen for map movement and zoom changes to trigger repopulation if (W && W.map && W.map.events && typeof W.map.events.register === 'function') { W.map.events.register("moveend", null, delayedRepopulateLayer); W.map.events.register("zoomend", null, delayedRepopulateLayer); console.log("WME Segment Completer: Registered map move/zoom handlers."); } else { console.error("WME Segment Completer: Could not register map event handlers."); } } // Debounce selection changes function delayedHandleSelectionChange() { if (selectionTimeout) { clearTimeout(selectionTimeout); } selectionTimeout = setTimeout(() => { handleSelectionChange(); }, SELECTION_DELAY); } // Debounce layer repopulation on map changes function delayedRepopulateLayer() { if (repopulateTimeout) { clearTimeout(repopulateTimeout); } repopulateTimeout = setTimeout(() => { // Don't repopulate if layer is hidden if (completionLayer && completionLayer.getVisibility()) { populateCompletionLayer(); } }, REPOPULATE_DELAY); } // Process the current selection to update the panel checkbox state function handleSelectionChange() { if (!W || !W.selectionManager || !W.model || !W.model.segments) { return; } const selectedFeatures = W.selectionManager.getSelectedWMEFeatures(); // Filter only for actual segment features returned by the new method const selectedSegments = selectedFeatures.filter(f => f && f.featureType === 'segment' && f.id != null); currentApplicableSegments = []; // Reset list for the panel checkbox isEntireStreetSelected = false; if (selectedSegments.length === 1) { // Single segment selected currentApplicableSegments = selectedSegments; isEntireStreetSelected = false; } else if (selectedSegments.length > 1) { // Multiple segments - check if they belong to the same street const firstSegmentId = selectedSegments[0].id; const firstSegmentModel = W.model.segments.get(firstSegmentId); // Get WME model object if (firstSegmentModel && firstSegmentModel.attributes) { const firstSegmentStreetId = firstSegmentModel.attributes.primaryStreetID; // Only proceed if the first segment has a name (street ID) if (firstSegmentStreetId != null) { // Check if all other selected segments have the same street ID const allMatch = selectedSegments.every(seg => { const segModel = W.model.segments.get(seg.id); return segModel && segModel.attributes && segModel.attributes.primaryStreetID === firstSegmentStreetId; }); if (allMatch) { currentApplicableSegments = selectedSegments; isEntireStreetSelected = true; } } } } // Update the "Mark as Complete" checkbox based on the analysis updateCheckboxArea(); } function updateCheckboxArea() { // Update the "Mark as Complete" checkbox area in the settings panel const checkboxArea = document.getElementById('wme-complete-checkbox-area'); if (!checkboxArea) { return; } checkboxArea.innerHTML = ''; // Clear previous content if (currentApplicableSegments.length > 0) { // Check if all currently selected applicable segments are in our completed list const allComplete = currentApplicableSegments.every(seg => { return seg && seg.id != null && !!completedSegments[seg.id]; }); // Create and add the checkbox const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `wme-complete-checkbox-dynamic`; checkbox.checked = allComplete; checkbox.addEventListener('change', (event) => { handleCheckboxChange(event.target.checked, currentApplicableSegments); }); label.appendChild(checkbox); const labelText = isEntireStreetSelected ? ' Mark Street as Complete' : ' Mark Segment as Complete'; label.appendChild(document.createTextNode(labelText)); checkboxArea.appendChild(label); } else { // Show placeholder message if no valid segment/street is selected const msgSpan = document.createElement('span'); msgSpan.id = 'wme-complete-no-segment-msg'; msgSpan.textContent = 'Select a segment or entire street'; checkboxArea.appendChild(msgSpan); } } function handleCheckboxChange(isChecked, segmentFeatures) { // Add/remove segments from the completed list and update the layer if (!completionLayer) { console.error("WME Segment Completer: Completion layer not available."); return; } if (!segmentFeatures || segmentFeatures.length === 0) { return; } console.log(`WME Segment Completer: Setting ${segmentFeatures.length} segments to complete=${isChecked}`); segmentFeatures.forEach(segmentFeature => { if (segmentFeature && segmentFeature.id != null) { const segmentId = segmentFeature.id; if (isChecked) { // Add segment if it's not already marked if (!completedSegments[segmentId]) { completedSegments[segmentId] = true; // Add feature to the layer immediately addFeatureToCompletionLayer(segmentId); } } else { // Remove segment if it is currently marked if (completedSegments[segmentId]) { delete completedSegments[segmentId]; // Always try to remove feature removeFeatureFromCompletionLayer(segmentId); } } } }); saveSettings(); // Save completion status changes } // --- Completion Layer Management --- function addFeatureToCompletionLayer(segmentId) { // Adds a visual highlight feature to our custom layer // *** Reverted: No bounds check here *** if (!completionLayer || !W || !W.model || !W.model.segments) return; const segmentModel = W.model.segments.get(segmentId); const geometry = segmentModel?.getOLGeometry ? segmentModel.getOLGeometry()?.clone() : segmentModel?.geometry?.clone(); if (!geometry) { console.warn(`WME Segment Completer: Could not find segment model or geometry for ID ${segmentId} when adding feature.`); return; } // Create the OL feature, storing the WME ID for reference const feature = new OpenLayers.Feature.Vector(geometry, { wmeSegmentId: segmentId }); completionLayer.addFeatures([feature]); // Map the WME ID to the new OL feature ID (assigned on add) if(feature.id) { featureMap[segmentId] = feature.id; } else { console.warn(`WME Segment Completer: Feature added for segment ${segmentId} but missing OL feature ID.`); } } function removeFeatureFromCompletionLayer(segmentId) { // Removes the highlight feature from our custom layer if (!completionLayer) return; const featureId = featureMap[segmentId]; // Look up OL feature ID from WME ID if (featureId) { const feature = completionLayer.getFeatureById(featureId); if (feature) { completionLayer.removeFeatures([feature], { silent: true }); } // Remove if found delete featureMap[segmentId]; // Clean up map entry } else { // Fallback if mapping is lost (e.g., after WME refresh/redraw) const featuresToRemove = completionLayer.features.filter(f => f.attributes && f.attributes.wmeSegmentId === segmentId); if (featuresToRemove.length > 0) { completionLayer.removeFeatures(featuresToRemove, { silent: true }); // console.log(`WME Segment Completer: Removed ${featuresToRemove.length} features via attribute search for segment ${segmentId}.`); } } } function populateCompletionLayer() { // Draw highlights for completed segments currently loaded in W.model // *** Reverted: No bounds check here *** if (!completionLayer || !W || !W.model || !W.model.segments) { return; } console.log("WME Segment Completer: Populating completion layer..."); completionLayer.removeAllFeatures({ silent: true }); // Clear layer first featureMap = {}; // Reset mapping let segmentsNotFound = 0; const segmentsToAdd = []; for (const segmentIdStr in completedSegments) { if (completedSegments.hasOwnProperty(segmentIdStr)) { const segmentId = parseInt(segmentIdStr, 10); const segmentModel = W.model.segments.get(segmentId); const geometry = segmentModel?.getOLGeometry ? segmentModel.getOLGeometry()?.clone() : segmentModel?.geometry?.clone(); if (geometry) { // Create feature if segment model and geometry exist in current W.model const feature = new OpenLayers.Feature.Vector(geometry, { wmeSegmentId: segmentId }); segmentsToAdd.push(feature); } else { // Segment ID is saved, but not found in the current W.model data // This is normal if it's off-screen. DO NOT delete it from completedSegments here. segmentsNotFound++; } } } // Add all valid features found in the current W.model at once if (segmentsToAdd.length > 0) { completionLayer.addFeatures(segmentsToAdd); // Create the ID map after features are added and get their OL IDs segmentsToAdd.forEach(f => { if (f.attributes && f.attributes.wmeSegmentId && f.id) { featureMap[f.attributes.wmeSegmentId] = f.id; } }); console.log(`WME Segment Completer: Added ${segmentsToAdd.length} features to completion layer.`); } if (segmentsNotFound > 0) { console.log(`WME Segment Completer: ${segmentsNotFound} completed segments not found in current W.model (likely off-screen).`); } if (segmentsToAdd.length === 0 && segmentsNotFound === 0) { console.log("WME Segment Completer: No completed segments found to populate layer."); } } function updateCompletionLayerStyles() { // Update the style definition for the entire layer if (!completionLayer || !completionLayer.styleMap) return; const newStyle = getCompletionStyle(); // Update the actual style object used by the layer completionLayer.styleMap.styles.default.defaultStyle = newStyle; completionLayer.redraw(); // Force redraw to apply changes console.log("WME Segment Completer: Updated completion layer style."); } function getCompletionStyle() { // Define the OpenLayers style for the highlight layer return { strokeColor: completedColor, strokeWidth: 8, // Thicker than default segments strokeOpacity: 0.6, // Slightly transparent strokeLinecap: "round", strokeDashstyle: "solid", }; } // --- Start the script --- // Use DOMContentLoaded listener for reliability if (document.readyState === 'complete' || document.readyState === 'interactive') { initialize(); } else { document.addEventListener('DOMContentLoaded', initialize); } })();