WME Segment Completer

Highlights completed WME segments on a separate map layer with a toggle. Repopulates layer on map change.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
    }

})();