WME Circles

Kreise im WME erstellen

目前為 2025-07-17 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME Circles
// @namespace    http://tampermonkey.net/
// @version      2025.01.17
// @description  Kreise im WME erstellen
// @namespace    https://greasyfork.org/de/users/863740-horst-wittlich
// @author       Hiwi234
// @match        https://*.waze.com/editor*
// @match        https://*.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        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

// Versions Format
// yyyy.mm.dd

(function() {
    'use strict';

    let wmeSDK;
    let uOpenLayers;
    let uWaze;
    let radiusLayer, infoLayer;
    let polygonControl, freehandControl;

    function addSidePanel() {
        wmeSDK.Sidebar.registerScriptTab('wme-circles').then(({ tabLabel, tabPane }) => {
            // Set tab label
            tabLabel.innerText = 'Circles';
            tabLabel.title = 'WME Circles';

            // Create the content of the side-panel tab
            tabPane.innerHTML = `
                <div style="padding: 15px;">
                    <h3>WME Circles v2025</h3>
                    <p id="circles-current-radius"></p>

                    <div style="margin: 15px 0;">
                        <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 8px;">
                            <input type="checkbox" id="wme-circles-edit-mode" style="margin-right: 8px;">
                            Enable Circle Drawing
                        </label>
                        <label style="display: flex; align-items: center; cursor: pointer;">
                            <input type="checkbox" id="wme-freehand-edit-mode" style="margin-right: 8px;">
                            Enable Free Hand Drawing
                        </label>
                    </div>

                    <div style="display: flex; flex-direction: column; gap: 10px; margin-top: 20px;">
                        <button id="wme-circles-select" style="width: 100%; padding: 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
                            Select Streets within
                        </button>
                        <button id="wme-circles-clear" style="width: 100%; padding: 8px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
                            Clear Circles
                        </button>
                    </div>

                    <div style="margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
                        <strong>Display Options:</strong>
                        <div style="margin-top: 8px;">
                            <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px;">
                                <input type="checkbox" id="show-radius" checked style="margin-right: 8px;">
                                Show Radius
                            </label>
                            <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px;">
                                <input type="checkbox" id="show-diameter" style="margin-right: 8px;">
                                Show Diameter
                            </label>
                            <label style="display: flex; align-items: center; cursor: pointer;">
                                <input type="checkbox" id="show-area" style="margin-right: 8px;">
                                Show Area
                            </label>
                        </div>
                    </div>

                    <div style="margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
                        <strong>Filter Options:</strong>
                        <div style="margin-top: 8px;">
                            <label style="display: flex; align-items: center; cursor: pointer; margin-bottom: 5px;">
                                <input type="checkbox" id="filter-drivable-only" checked style="margin-right: 8px;">
                                Only drivable segments
                            </label>
                        </div>
                    </div>
                </div>
            `;

            let checkbox = tabPane.querySelector('#wme-circles-edit-mode');
            let freehandCheckbox = tabPane.querySelector('#wme-freehand-edit-mode');
            let clearButton = tabPane.querySelector('#wme-circles-clear');
            let selectButton = tabPane.querySelector('#wme-circles-select');
            let showRadiusCheckbox = tabPane.querySelector('#show-radius');
            let showDiameterCheckbox = tabPane.querySelector('#show-diameter');
            let showAreaCheckbox = tabPane.querySelector('#show-area');
            let filterDrivableCheckbox = tabPane.querySelector('#filter-drivable-only');

            // Update annotations when display options change
            function updateAllAnnotations() {
                // Clear current annotations
                infoLayer.destroyFeatures();

                // Re-add annotations for all features
                for (let feature of radiusLayer.features) {
                    if (feature.geometry.CLASS_NAME === "OpenLayers.Geometry.Polygon") {
                        // Check if it's a circle (regular polygon with ~100 sides) or freehand
                        let components = feature.geometry.components[0].components;
                        if (components.length > 90) {
                            // Likely a circle
                            addCircleAnnotations(feature);
                        } else {
                            // Likely freehand
                            addFreehandAnnotations(feature);
                        }
                    }
                }
            }

            showRadiusCheckbox.addEventListener('change', updateAllAnnotations);
            showDiameterCheckbox.addEventListener('change', updateAllAnnotations);
            showAreaCheckbox.addEventListener('change', updateAllAnnotations);

            function addCircleAnnotations(f) {
                let minX = f.geometry.bounds.left;
                let minY = f.geometry.bounds.bottom;
                let maxX = f.geometry.bounds.right;
                let maxY = f.geometry.bounds.top;

                let startX = (minX + maxX) / 2;
                let startY = (minY + maxY) / 2;

                let startPoint = new uOpenLayers.Geometry.Point(startX, startY);
                let endPoint = new uOpenLayers.Geometry.Point(maxX, startY);
                let radius = new uOpenLayers.Geometry.LineString([startPoint, endPoint]);
                let len = radius.getGeodesicLength(new uOpenLayers.Projection("EPSG:900913"));

                // Calculate area (circle area = π * r²)
                let area = Math.PI * len * len;

                let showRadius = showRadiusCheckbox?.checked;
                let showDiameter = showDiameterCheckbox?.checked;
                let showArea = showAreaCheckbox?.checked;

                let centerStyle = {
                    strokeColor: "#c40606",
                    strokeWidth: 2,
                    pointRadius: 5,
                    fillOpacity: 0.2
                };

                let labelText = [];

                // Add radius info
                if (showRadius) {
                    let unit = 'm';
                    let displayLen = len;
                    if (len > 1000) {
                        displayLen = Math.round((len / 1000) * 100) / 100;
                        unit = "km";
                    } else {
                        displayLen = Math.round(len);
                    }
                    labelText.push('R: ' + displayLen + ' ' + unit);
                }

                // Add diameter info
                if (showDiameter) {
                    let diameter = len * 2;
                    let unit = 'm';
                    let displayDiam = diameter;
                    if (diameter > 1000) {
                        displayDiam = Math.round((diameter / 1000) * 100) / 100;
                        unit = "km";
                    } else {
                        displayDiam = Math.round(diameter);
                    }
                    labelText.push('Ø: ' + displayDiam + ' ' + unit);
                }

                // Add area info
                if (showArea) {
                    let unit = 'm²';
                    let displayArea = area;
                    if (area > 1000000) {
                        displayArea = Math.round((area / 1000000) * 100) / 100;
                        unit = "km²";
                    } else {
                        displayArea = Math.round(area);
                    }
                    labelText.push('A: ' + displayArea + ' ' + unit);
                }

                let lineStyle = {
                    strokeColor: "#c40606",
                    strokeWidth: 3,
                    label: labelText.join(' | '),
                    labelAlign: "left",
                    labelXOffset: "20",
                    labelYOffset: "10",
                    labelOutlineColor: "white",
                    labelOutlineWidth: 3
                };

                let center = new uOpenLayers.Feature.Vector(startPoint, {}, centerStyle);
                if (labelText.length > 0) {
                    let radiusLine = new uOpenLayers.Feature.Vector(radius, { 'length': len }, lineStyle);
                    infoLayer.addFeatures([center, radiusLine]);
                } else {
                    infoLayer.addFeatures([center]);
                }
            }

            function addFreehandAnnotations(f) {
                let bounds = f.geometry.getBounds();
                let minX = bounds.left;
                let minY = bounds.bottom;
                let maxX = bounds.right;
                let maxY = bounds.top;

                let centerX = (minX + maxX) / 2;
                let centerY = (minY + maxY) / 2;
                let width = maxX - minX;
                let height = maxY - minY;
                let diameter = Math.max(width, height);

                let centerPoint = new uOpenLayers.Geometry.Point(centerX, centerY);
                let endPoint = new uOpenLayers.Geometry.Point(centerX + diameter/2, centerY);
                let diameterLine = new uOpenLayers.Geometry.LineString([centerPoint, endPoint]);
                let len = diameterLine.getGeodesicLength(new uOpenLayers.Projection("EPSG:900913"));

                let area = f.geometry.getArea();

                let showRadius = showRadiusCheckbox?.checked;
                let showDiameter = showDiameterCheckbox?.checked;
                let showArea = showAreaCheckbox?.checked;

                let centerStyle = {
                    strokeColor: "#c40606",
                    strokeWidth: 2,
                    pointRadius: 5,
                    fillOpacity: 0.2
                };

                let labelText = [];

                if (showRadius) {
                    let radius = len / 2;
                    let unit = 'm';
                    let displayRadius = radius;
                    if (radius > 1000) {
                        displayRadius = Math.round((radius / 1000) * 100) / 100;
                        unit = "km";
                    } else {
                        displayRadius = Math.round(radius);
                    }
                    labelText.push('R: ' + displayRadius + ' ' + unit);
                }

                if (showDiameter) {
                    let unit = 'm';
                    let displayLen = len;
                    if (len > 1000) {
                        displayLen = Math.round((len / 1000) * 100) / 100;
                        unit = "km";
                    } else {
                        displayLen = Math.round(len);
                    }
                    labelText.push('Ø: ' + displayLen + ' ' + unit);
                }

                if (showArea) {
                    let unit = 'm²';
                    let displayArea = area;
                    if (area > 1000000) {
                        displayArea = Math.round((area / 1000000) * 100) / 100;
                        unit = "km²";
                    } else {
                        displayArea = Math.round(area);
                    }
                    labelText.push('A: ' + displayArea + ' ' + unit);
                }

                let lineStyle = {
                    strokeColor: "#c40606",
                    strokeWidth: 3,
                    label: labelText.join(' | '),
                    labelAlign: "left",
                    labelXOffset: "20",
                    labelYOffset: "10",
                    labelOutlineColor: "white",
                    labelOutlineWidth: 3
                };

                let center = new uOpenLayers.Feature.Vector(centerPoint, {}, centerStyle);
                if (labelText.length > 0) {
                    let diameterLineFeature = new uOpenLayers.Feature.Vector(diameterLine, { 'diameter': len }, lineStyle);
                    infoLayer.addFeatures([center, diameterLineFeature]);
                } else {
                    infoLayer.addFeatures([center]);
                }
            }

            checkbox.addEventListener('click', (e) => {
                if (e.target.checked) {
                    freehandCheckbox.checked = false;
                    freehandControl.deactivate();
                    polygonControl.activate();
                } else {
                    polygonControl.deactivate();
                }
            });

            freehandCheckbox.addEventListener('click', (e) => {
                if (e.target.checked) {
                    checkbox.checked = false;
                    polygonControl.deactivate();
                    freehandControl.activate();
                } else {
                    freehandControl.deactivate();
                }
            });

            clearButton.addEventListener('click', (e) => {
                infoLayer.destroyFeatures();
                radiusLayer.destroyFeatures();
                checkbox.checked = false;
                freehandCheckbox.checked = false;
                polygonControl.deactivate();
                freehandControl.deactivate();
            });

            selectButton.addEventListener('click', (e) => {
                const toSelect = [];
                const segments = uWaze.model.segments.getObjectArray();

                for (const segment of segments) {
                    if (!segment.attributes?.roadType) continue;

                    if (filterDrivableCheckbox.checked) {
                        const type = segment.attributes.roadType;
                        if (type < 1 || type > 6) continue; // only street to freeway
                    }

                    const segGeom = segment.getOLGeometry ? segment.getOLGeometry() : segment.geometry;
                    if (!segGeom) continue;

                    for (const drawnFeature of radiusLayer.features) {
                        if (drawnFeature.geometry.intersects(segGeom)) {
                            toSelect.push(segment);
                            break;
                        }
                    }
                }

                console.log(`[WME Circles] Selected ${toSelect.length} segments`);

                if (toSelect.length > 0) {
                    uWaze.selectionManager.setSelectedModels(toSelect);
                }

                checkbox.checked = false;
                freehandCheckbox.checked = false;
                polygonControl.deactivate();
                freehandControl.deactivate();
            });
        });
    }

    function radiusInit() {
        radiusLayer = new uOpenLayers.Layer.Vector("WME Circle Control Layer", {
            displayInLayerSwitcher: true,
            uniqueName: "__CircleControlLayer",
            visibility: true,
            style: {
                "fillColor": "#c40606",
                "fillOpacity": 0.2,
                "strokeColor": "#c40606",
                "strokeOpacity": 1,
                "strokeWidth": 1,
                "strokeLinecap": "round",
                "strokeDashstyle": "solid",
                "pointRadius": 6,
                "pointerEvents": "visiblePainted",
                "labelAlign": "cm",
                "labelOutlineColor": "white",
                "labelOutlineWidth": 3
            }
        });

        infoLayer = new uOpenLayers.Layer.Vector("WME Circle Visual Layer", {
            displayInLayerSwitcher: true,
            uniqueName: "__DrawCircleDisplayLayer",
            visibility: true
        });

        let polygonHandler = uOpenLayers.Handler.RegularPolygon;
        polygonControl = new uOpenLayers.Control.DrawFeature(radiusLayer, polygonHandler, {
            handlerOptions: {
                sides: 100
            }
        });

        // Add free hand drawing control with Polygon handler for closed shapes
        let polygonHandlerFreehand = uOpenLayers.Handler.Polygon;
        freehandControl = new uOpenLayers.Control.DrawFeature(radiusLayer, polygonHandlerFreehand, {
            handlerOptions: {
                freehand: true,
                freehandToggle: null
            }
        });

        uWaze.map.addLayer(radiusLayer);
        uWaze.map.addLayer(infoLayer);
        uWaze.map.addControl(polygonControl);
        uWaze.map.addControl(freehandControl);

        addSidePanel();

        polygonControl.handler.callbacks.move = function (e) {
            let linearRing = new uOpenLayers.Geometry.LinearRing(e.components[0].components);
            let geometry = new uOpenLayers.Geometry.Polygon([linearRing]);
            let polygonFeature = new uOpenLayers.Feature.Vector(geometry, null);
            let polybounds = polygonFeature.geometry.getBounds();
            let minX = polybounds.left;
            let minY = polybounds.bottom;
            let maxX = polybounds.right;
            let maxY = polybounds.top;
            let startX = (minX + maxX) / 2;
            let startY = (minY + maxY) / 2;
            let startPoint = new uOpenLayers.Geometry.Point(startX, startY);
            let endPoint = new uOpenLayers.Geometry.Point(maxX, startY);
            let radius = new uOpenLayers.Geometry.LineString([startPoint, endPoint]);
            let len = radius.getGeodesicLength(new uOpenLayers.Projection("EPSG:900913"));

            let unit = 'm';
            if (len > 1000) {
                len = Math.round((len / 1000) * 100) / 100;
                unit = "km";
            } else {
                len = Math.round(len);
            }

            let rad = document.getElementById('circles-current-radius');
            if (rad) {
                rad.innerHTML = 'Current Radius: ' + len + ' ' + unit;
            }

            // Show radius on the map while drawing - clear ALL annotations first
            infoLayer.destroyFeatures();

            let centerStyle = {
                strokeColor: "#ff9800",
                strokeWidth: 2,
                pointRadius: 3,
                fillOpacity: 0.3
            };

            let lineStyle = {
                strokeColor: "#ff9800",
                strokeWidth: 2,
                label: len + ' ' + unit,
                labelAlign: "left",
                labelXOffset: "15",
                labelYOffset: "5",
                labelOutlineColor: "white",
                labelOutlineWidth: 2,
                strokeDashstyle: "dash"
            };

            let center = new uOpenLayers.Feature.Vector(startPoint, {}, centerStyle);
            let radiusLine = new uOpenLayers.Feature.Vector(radius, {}, lineStyle);

            infoLayer.addFeatures([center, radiusLine]);
        }

        polygonControl.events.on({
            'featureadded': function (e) {
                let rad = document.getElementById('circles-current-radius');
                if (rad) {
                    rad.innerHTML = '';
                }
                // Add a small delay to ensure the feature is fully added, then update annotations
                setTimeout(() => {
                    updateAllAnnotations();
                }, 100);
            }
        });

        freehandControl.events.on({
            'featureadded': function (e) {
                let rad = document.getElementById('circles-current-radius');
                if (rad) {
                    rad.innerHTML = '';
                }
                // Add a small delay to ensure the feature is fully added, then update annotations
                setTimeout(() => {
                    updateAllAnnotations();
                }, 100);
            }
        });
    }

    function radiusBootstrap() {
        // Initialize SDK first
        if (!window.getWmeSdk) {
            setTimeout(radiusBootstrap, 500);
            return;
        }

        wmeSDK = window.getWmeSdk({
            scriptId: 'wme-circles',
            scriptName: 'WME Circles'
        });

        // Wait for WME to be ready
        wmeSDK.Events.once({ eventName: "wme-ready" }).then(() => {
            // Now get the traditional objects
            uWaze = window.W || unsafeWindow.W;
            uOpenLayers = window.OpenLayers || unsafeWindow.OpenLayers;

            if (typeof (uOpenLayers) === 'undefined' || typeof (uWaze) === 'undefined' || typeof (uWaze.map) === 'undefined') {
                setTimeout(radiusBootstrap, 500);
            } else {
                radiusInit();
            }
        });
    }

    // Initialize when SDK is available
    if (window.SDK_INITIALIZED) {
        window.SDK_INITIALIZED.then(radiusBootstrap);
    } else {
        radiusBootstrap();
    }

})();