WME Segment Completer

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

})();