WME Polygon Validator

Real-time warning when a polygon becomes invalid (self-intersecting) in WME

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         WME Polygon Validator
// @namespace    https://github.com/Metroseksuaali
// @version      1.1.0
// @description  Real-time warning when a polygon becomes invalid (self-intersecting) in WME
// @author       Metro
// @match        https://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @require      https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_ID = 'wme-polygon-validator';
    const SCRIPT_NAME = 'WME Polygon Validator';
    const SCRIPT_VERSION = '1.1.0';

    let wmeSDK = null;
    let warningBanner = null;
    let kinkMarkers = [];
    let mapLayer = null;

    // ============================================
    // Initialization
    // ============================================

    function log(message) {
        console.log(`[${SCRIPT_NAME}] ${message}`);
    }

    function init() {
        if (typeof window.SDK_INITIALIZED !== 'undefined') {
            window.SDK_INITIALIZED.then(initWithSDK);
        } else {
            waitForSDK();
        }
    }

    function waitForSDK() {
        if (typeof window.SDK_INITIALIZED !== 'undefined') {
            window.SDK_INITIALIZED.then(initWithSDK);
        } else if (typeof window.getWmeSdk !== 'undefined') {
            initWithSDK();
        } else {
            setTimeout(waitForSDK, 500);
        }
    }

    function initWithSDK() {
        try {
            if (typeof window.getWmeSdk === 'function') {
                wmeSDK = window.getWmeSdk({
                    scriptId: SCRIPT_ID,
                    scriptName: SCRIPT_NAME
                });
            }
        } catch (e) {
            // SDK initialization failed, continue with fallback
        }

        waitForWME();
    }

    function waitForWME() {
        if (window.W && window.W.map && window.W.model && window.W.selectionManager) {
            setupValidator();
        } else {
            setTimeout(waitForWME, 500);
        }
    }

    // ============================================
    // Validator setup
    // ============================================

    function setupValidator() {
        createMapLayer();
        createWarningBanner();

        // Listen for selection changes
        window.W.selectionManager.events.register('selectionchanged', null, onSelectionChanged);

        // Listen for edit actions
        window.W.model.actionManager.events.register('afteraction', null, onAfterAction);
        window.W.model.actionManager.events.register('afterundoaction', null, onAfterAction);
        window.W.model.actionManager.events.register('afterclearactions', null, clearWarnings);

        log('Initialized');

        // Initial check if something is already selected
        setTimeout(validateGeometry, 500);
    }

    // ============================================
    // Map layer
    // ============================================

    function createMapLayer() {
        if (window.OpenLayers) {
            mapLayer = new window.OpenLayers.Layer.Vector(SCRIPT_NAME, {
                displayInLayerSwitcher: false,
                uniqueName: SCRIPT_ID
            });
            window.W.map.addLayer(mapLayer);
        }
    }

    // ============================================
    // Warning banner
    // ============================================

    function createWarningBanner() {
        warningBanner = document.createElement('div');
        warningBanner.id = 'wme-polygon-validator-warning';
        warningBanner.style.cssText = `
            position: fixed;
            top: 60px;
            left: 50%;
            transform: translateX(-50%);
            background-color: #dc3545;
            color: white;
            padding: 12px 24px;
            border-radius: 6px;
            font-size: 14px;
            font-weight: bold;
            z-index: 10000;
            display: none;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            text-align: center;
            max-width: 90%;
        `;
        warningBanner.innerHTML = `
            <span style="margin-right: 8px;">⚠️</span>
            <span id="wme-pv-message">Polygon is self-intersecting!</span>
            <span style="margin-left: 12px; font-weight: normal; font-size: 12px;">
                Fix the shape before saving
            </span>
        `;
        document.body.appendChild(warningBanner);
    }

    function showWarning(kinkCount) {
        if (warningBanner) {
            const message = document.getElementById('wme-pv-message');
            if (message) {
                if (kinkCount === 1) {
                    message.textContent = 'Polygon is self-intersecting at 1 point!';
                } else {
                    message.textContent = `Polygon is self-intersecting at ${kinkCount} points!`;
                }
            }
            warningBanner.style.display = 'block';
        }
    }

    function hideWarning() {
        if (warningBanner) {
            warningBanner.style.display = 'none';
        }
    }

    // ============================================
    // Map markers (intersection points)
    // ============================================

    function drawKinkMarkers(kinks) {
        clearKinkMarkers();

        if (!mapLayer || !window.OpenLayers || !kinks || !kinks.features) {
            return;
        }

        kinks.features.forEach(kink => {
            const coords = kink.geometry.coordinates;
            const lonLat = new window.OpenLayers.LonLat(coords[0], coords[1]);
            const projectedLonLat = lonLat.transform(
                new window.OpenLayers.Projection('EPSG:4326'),
                window.W.map.getProjectionObject()
            );

            const point = new window.OpenLayers.Geometry.Point(projectedLonLat.lon, projectedLonLat.lat);
            const style = {
                pointRadius: 12,
                fillColor: '#dc3545',
                fillOpacity: 0.9,
                strokeColor: '#ffffff',
                strokeWidth: 3,
                strokeOpacity: 1
            };

            const feature = new window.OpenLayers.Feature.Vector(point, null, style);
            kinkMarkers.push(feature);
            mapLayer.addFeatures([feature]);
        });
    }

    function clearKinkMarkers() {
        if (mapLayer && kinkMarkers.length > 0) {
            mapLayer.removeFeatures(kinkMarkers);
        }
        kinkMarkers = [];
    }

    function clearWarnings() {
        hideWarning();
        clearKinkMarkers();
    }

    // ============================================
    // Geometry retrieval
    // ============================================

    function getSelectedVenue() {
        let selection;
        if (window.W.selectionManager.getSelectedWMEFeatures) {
            selection = window.W.selectionManager.getSelectedWMEFeatures();
        } else {
            selection = window.W.selectionManager.getSelectedFeatures();
        }

        if (!selection || selection.length === 0) {
            return null;
        }

        const feature = selection[0];
        let venue = null;

        if (feature && feature.model) {
            venue = feature.model;
        } else if (feature && feature.type === 'venue') {
            venue = feature;
        } else if (feature && feature._wmeObject) {
            venue = feature._wmeObject;
        } else if (feature && feature.attributes && feature.attributes.wazeFeature) {
            venue = feature.attributes.wazeFeature._wmeObject || feature.attributes.wazeFeature;
        }

        if (!venue || venue.type !== 'venue') {
            return null;
        }

        return venue;
    }

    function getVenueGeometry(venue) {
        if (!venue) return null;

        if (typeof venue.getGeometry === 'function') {
            return venue.getGeometry();
        } else if (venue.geometry) {
            return venue.geometry;
        } else if (venue.attributes && venue.attributes.geometry) {
            return venue.attributes.geometry;
        }

        return null;
    }

    function isAreaVenue(venue) {
        if (!venue) return false;

        if (typeof venue.isPoint === 'function') {
            return !venue.isPoint();
        }

        if (venue.attributes && venue.attributes.geometry) {
            const geomType = venue.attributes.geometry.type;
            return geomType === 'Polygon' || geomType === 'MultiPolygon';
        }

        const geometry = getVenueGeometry(venue);
        if (geometry && geometry.CLASS_NAME) {
            return geometry.CLASS_NAME.includes('Polygon');
        }

        return true;
    }

    // ============================================
    // Geometry conversion to GeoJSON
    // ============================================

    function wmeGeometryToGeoJSON(geometry) {
        if (!geometry) {
            return null;
        }

        try {
            // WME uses OpenLayers geometry (Spherical Mercator EPSG:900913/3857)
            if (geometry.components && geometry.CLASS_NAME) {
                if (geometry.CLASS_NAME.includes('Polygon')) {
                    const coordinates = [];

                    geometry.components.forEach(ring => {
                        const ringCoords = [];
                        if (ring.components) {
                            ring.components.forEach(point => {
                                // Convert from WME projection (EPSG:900913) to WGS84 (EPSG:4326)
                                const lonLat = new window.OpenLayers.LonLat(point.x, point.y);
                                const wgs84LonLat = lonLat.transform(
                                    window.W.map.getProjectionObject(),
                                    new window.OpenLayers.Projection('EPSG:4326')
                                );
                                ringCoords.push([wgs84LonLat.lon, wgs84LonLat.lat]);
                            });
                        }
                        if (ringCoords.length > 0) {
                            coordinates.push(ringCoords);
                        }
                    });

                    if (coordinates.length > 0 && coordinates[0].length >= 4) {
                        return turf.polygon(coordinates);
                    }
                    return null;
                }
            }

            // Geometry is already in GeoJSON format
            if (geometry.type === 'Polygon' && geometry.coordinates) {
                return turf.polygon(geometry.coordinates);
            }

            return null;
        } catch (e) {
            return null;
        }
    }

    // ============================================
    // Validation
    // ============================================

    function validateGeometry() {
        const venue = getSelectedVenue();
        if (!venue) {
            clearWarnings();
            return;
        }

        if (!isAreaVenue(venue)) {
            clearWarnings();
            return;
        }

        const geometry = getVenueGeometry(venue);
        if (!geometry) {
            clearWarnings();
            return;
        }

        const geoJSON = wmeGeometryToGeoJSON(geometry);
        if (!geoJSON) {
            clearWarnings();
            return;
        }

        try {
            const kinks = turf.kinks(geoJSON);

            if (kinks && kinks.features && kinks.features.length > 0) {
                showWarning(kinks.features.length);
                drawKinkMarkers(kinks);
            } else {
                clearWarnings();
            }
        } catch (e) {
            clearWarnings();
        }
    }

    // ============================================
    // Event handlers
    // ============================================

    function onSelectionChanged() {
        setTimeout(validateGeometry, 150);
    }

    function onAfterAction() {
        setTimeout(validateGeometry, 100);
    }

    // ============================================
    // Start script
    // ============================================

    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }

})();