WME Simple Alert Messages

changes the styling of the segment and venue messages

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME Simple Alert Messages
// @namespace    https://fxzfun.com/
// @version      3.3.0
// @description  changes the styling of the segment and venue messages
// @author       FXZFun
// @match        https://*.waze.com/*/editor*
// @match        https://*.waze.com/editor*
// @exclude      https://*.waze.com/user/editor*
// @icon         data:image/webp;base64,UklGRi4EAABXRUJQVlA4TCEEAAAvgoAgEJVIb4A3y5zrJscm7yg3Skd5R2GUm6SWDsiHH5uq/4ISESBWTZBt0/tL3mQG0rbJ2YTcv8nNv0NJVtvmoe4mgwxqyQeU8qspYaIkrojakUpkB4b+7I4wlRwa8smyLwUztA0ICT+jJYNyWEZiqKWoNwaeinxGSpqzwMSwhtOblHMs2SWyThY/KPPYArhDE3qDJCY1Xaz4cMQ/y3gi0S9GlkdcFoINoXoRG6hhiWWjYdlh4E+G0sYekOWwykRZp4a/B1SZ7uAbIe5terAPAjS1jNlJsmzR7G7L1WUod01Whmgzf1FN9M1MWlVrqVy4jpsqLLURQTO/+EkwvXjBxVMf908NJne/ljf4D0u8uaQmOZX33e8oTfWdT5vaLgW/iYxO7/2O03XzU4KjZFSEM32d6mFmM7Cip7mZIMXbxtp3NK3xoNTauWCqu6Np497EmPx13P11bSDJ+qb2J+iOG1xgg1K3Ud1G/PnuB16lBjiIoGeNN/E2ZwODWFvvNwug9VeOFrDQuxpQqsYkwUHbhWKtaDt12eNdHCpDDzcgCSGY6i1awNSw0ExJwSRTqszTgfUHBLJcmcM7T/9wpujiE4XOBOZgkrn+qVFcCKWSgeocB9xttFhGWCdh5sMxbDPB+qgpV4kNO/f1eN97w8ClysSAcvMaHGiSA0oFGR5nu1AsAE1LPyWZrsoRNBn6qOZpZVOSybJNYbOospMwyc3kRYqqMhE4EjiZmoih8QSUbB/xwddbY8uQZbVz5xonXz/F/ZB3yrHGyx8R9+RN7wKQLYlrjadq1sp1OzOU02bJqsEBWVvgRC5DMdOElFlzZcgTH5XFLZo1uSCbLjtElqzgScsRmLDlXdiE66No9a+Ui01cMWXghbIn4GvAlikchJxZKqmx40Khel9b/5okMW7/8mtyU6XMjzJvUmfWfHrTe6AXhYzSxXc9bItBk0Qb2XN7SULV2RZO71hMCY3OlE+8X8yLk8go0IgSqsOX6n7WAQosoJpTXA347NXjdUeySljTSrjB4avla6aV8mfmwNMRcDU6iOgs+wWOSApnYV43lMRbXaxS2l4gHhmSukLuy2dAssbHzxe+GXIuICPAdTcY8p7ZuFyosI4jG5Qy94W9fQ8Wni9/m1FG33d56PLCU/1TiOz+CBgFBApjkwiU/I2uIfWz9xS725Tt26yhwmj3h1LTrsRr1UFaGd7nkrrRzhi2v5/u3t2DfQ9YxjKtpGVedDwBw0sJa2tNwRZFU7s503Z4V2cb/whk/F+c/MZJVnib53mvokGwTRwaYPyhKLAQOyz9ekuSE9kfyBK2gT6novReETaVov/3EaUU/cqvpDll8igRUGLessFrV0pmRMlcSgmMSymajeC+/NC+FCx/jfl7ocz5zwOlpR+O1kSLnlEAAA==
// @grant        none
// @license      MIT
// ==/UserScript==

/* global W, OpenLayers, trustedTypes */

(function() {
    'use strict';
    const DEBUG = false;
    const SCRIPT_VERSION = GM_info.script.version.toString();
    const SCRIPT_NAME = GM_info.script.name;

    class Messages {
        static LOCKED = "locked";
        static EA = "ea";
        static MIXED = "mixed";
        static PUR = "pur";
        static CLOSURE = "closure";
        static HIDE_MESSAGE = "hide_message";
        static SNAPSHOT_ACTIVE = "snapshot";
        static ROUTING_PREFERENCE = "routing";
        static JUNCTION_BOX = "junction_box";

        static messages = {
            "locked":        { "icon": "w-icon-lock-fill", "message": "Locked to L{maxLock}" },
            "ea":            { "icon": "w-icon-alert-fill", "message": "Out of your EA" },
            "mixed":         { "icon": "w-icon-round-trip", "message": "Mixed A/B" },
            "pur":           { "icon": "w-icon-location-update-fill", "message": "PUR Request" },
            "closure":       { "icon": "w-icon-closure", "message": "Has Closures" },
            "snapshot":      { "icon": "w-icon-restore", "message": "Snapshot Mode On" },
            "routing":       { "icon": "w-icon-route", "message": "Routing Pref" },
            "junction_box":  { "icon": "w-icon-polygon", "message": "In Junction Box" }
        }

        static getMessage(type, maxLock='') {
            return this.messages?.[type]?.message?.replace('{maxLock}', maxLock);
        }

        static getIcon(type) {
            return this.messages?.[type]?.icon;
        }
    }

    class MessageContainer {
        container;

        constructor() {
            this.container = document.getElementById("wmeSamContainer") || document.createElement("div");
            if (!document.getElementById("wmeSamContainer")) {
                this.container.id = "wmeSamContainer";
                document.querySelector("#edit-panel wz-tabs").insertAdjacentElement("beforeBegin", this.container);
            }
        }

        reset() {
            this.container.innerHTML = policy.createHTML("");
        }

        addMessage(messageType, maxLock='') {
            this.container.insertAdjacentHTML("beforeEnd", policy.createHTML(`<span class="wmeSamMessage ${messageType}"><i class="w-icon ${Messages.getIcon(messageType)}"></i> ${Messages.getMessage(messageType, maxLock) ?? ''}</span>`));
        }
    }

    const codeNames = {'400': 'Bad Request', '401': 'Unauthorized', '402': 'Payment Required', '403': 'Forbidden', '404': 'Not Found', '405': 'Method Not Allowed', '406': 'Not Acceptable', '407': 'Proxy Authentication Required', '408': 'Request Timeout', '409': 'Conflict', '410': 'Gone', '411': 'Length Required', '412': 'Precondition Required', '413': 'Request Entry Too Large', '414': 'Request-URI Too Long', '415': 'Unsupported Media Type', '416': 'Requested Range Not Satisfiable', '417': 'Expectation Failed', '418': 'I\'m a teapot', '429': 'Too Many Requests', '500': 'Internal Server Error', '501': 'Not Implemented', '502': 'Bad Gateway', '503': 'Service Unavailable', '504': 'Gateway Timeout', '505': 'HTTP Version Not Supported'};
    const policy = trustedTypes.createPolicy('wmeSamPolicy', { createHTML: (input) => input});

    function getUrl() {
        let center = W.map.getCenter();
        let lonlat = new OpenLayers.LonLat(center.lon, center.lat);
        lonlat.transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:4326'));
        let zoom = W.map.getZoom();

        let features = W.selectionManager.getSelectedFeatures();
        let featureType = features[0]._wmeObject.getType()
        features = features.map(f => f.id);

        let url = location.href.split("?")[0] + `?env=${W.map.wazeMap.regionCode}&lat=${lonlat.lat}&lon=${lonlat.lon}&zoomLevel=${zoom}`;
        if (features.length > 0) url += `&${featureType}s=${features.join(",")}`;
        return url;
    }

    function getCity() {
        var city = document.querySelector(".location-info").innerText.split(",")[0];
        if (document.querySelector(".wmecitiesoverlay-region") !== null) city = document.querySelector(".wmecitiesoverlay-region").innerText;
        return city;
    }

    function addLockRequestCopier(maxLock, routingType=false) {
        let chip = document.querySelector(".wmeSamMessage.locked") ?? document.querySelector(".wmeSamMessage.routing");
        chip.addEventListener("click", () => {
            navigator.clipboard.writeText(`:unlock${maxLock}: ${routingType ? ':arrow_down_small:' : ''} ${getCity()} - *reason* - <${getUrl()}>`);
            chip.innerHTML = policy.createHTML(`<span class="wmeSamMessage locked"><i class="w-icon w-icon-copy"></i> Copied!</span>`);
        });
    }

    function isOOEA(id) {
        if ((typeof id === "string" && id.includes("OpenLayers")) || W.snapshotManager.isSnapshotOn()) return false; // not OOEA

        const type = typeof id === "string" && id.includes(".") ? "venues" : "segments";
        const model = W.model[type].objects[id];

        const userRank = W.loginManager.getUserRank();
        const lockRank = model?.getLockRank();
        const propertiesEditable = model.arePropertiesEditable();

        if (propertiesEditable) return false; // not OOEA
        if (lockRank > userRank) return type === "venues"; // locked up
        if (userRank >= lockRank) return !propertiesEditable; // unlocked but not editable
        return false;
    }


    window.isOOEA = isOOEA;

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function getPanel() {
        for (let i = 0; i < 100; i++) {
            const panel = document.querySelector("#edit-panel");
            const tabs = document.querySelector("#edit-panel wz-tabs");
            if (panel && tabs) return panel;
            await sleep(100);
        }
    }

    async function addAdvancedErrorDetails(response) {
        const parsedJSON = await response.json();
        const div = document.createElement("div");

        function createA(lat, lng) {
            const a = document.createElement("a");
            a.innerText = `${lat}, ${lng}`;
            a.href = `https://www.waze.com/editor?lat=${lat}&lon=${lng}&zoom=20`;
            a.onclick = (ev) => {
                ev.preventDefault();
                const center = OpenLayers.Layer.SphericalMercator.forwardMercator(lng, lat);
                W.map.setCenter(center);
                document.querySelector(".error-list .close-button").click();
            };
            return a;
        }

        const errorDetailsHTML = policy.createHTML(`
        <div class="wmeSamAdvancedErrorDetails">
            <wz-caption>Server Response</wz-caption>
            <p>Error ${response.status}: ${codeNames[response.status]}</p>
            <wz-caption>Internal Messages [${parsedJSON.errorList.length}]</wz-caption>
        </div>`);

        div.innerHTML = errorDetailsHTML;
        document.querySelector(".error-list .body").appendChild(div);

        const errorListContainer = div.children[0];

        parsedJSON.errorList.forEach(e => {
            errorListContainer.innerHTML += policy.createHTML(`<p>Error ${e.code}: ${e.details}</p>`);
            errorListContainer.appendChild(createA(e.geometry.coordinates[1], e.geometry.coordinates[0]));
        });
    }

    async function processSelectionChange() {
        let selected = W.selectionManager.getSelectedFeatures();
        let userRank = W.loginManager.user.getRank();
        let maxLockRank = Math.max(...selected.map(item => item._wmeObject.attributes.lockRank));
        let maxRoutingType = Math.max(...selected.map(s => s._wmeObject.getAttribute("routingRoadType")));

        if (selected.length === 0) return;

        // wait for panel to open
        let panel = await getPanel();
        if (!panel) {
            console.error("WME SAM: Could not open panel");
            return;
        }

        // add or clear message container
        let messageContainer = new MessageContainer();
        messageContainer.reset();

        const wzAlerts = Array.from(document.querySelector('wz-alerts-group')?.children || []);
        wzAlerts.forEach(wzAlert => {
            let messageType;

            if (userRank < maxLockRank) messageType = Messages.LOCKED;
            if (wzAlert?.innerHTML?.includes('driven area') || (maxLockRank <= userRank && wzAlert?.innerHTML?.includes('editing area'))) messageType = Messages.EA;
            if (wzAlert?.classList?.contains('inconsistent-direction-alert')) messageType = Messages.MIXED;
            if (wzAlert?.innerHTML?.includes('pending')) messageType = Messages.PUR;
            if (wzAlert?.innerHTML?.includes('active road closures')) messageType = Messages.CLOSURE;
            if (wzAlert?.innerHTML?.includes('junction box')) messageType = Messages.JUNCTION_BOX;
            if (wzAlert?.innerHTML?.includes('reviewed')) messageType = Messages.HIDE_MESSAGE;

            if (!!messageType) {
                if (messageType !== Messages.HIDE_MESSAGE) messageContainer.addMessage(messageType, maxLockRank + 1);
                if (!DEBUG) wzAlert?.classList?.add("hide");
                if (DEBUG) console.log("WME SAM: " + messageType);
            }
            if (messageType === Messages.LOCKED) addLockRequestCopier(maxLockRank + 1, !!maxRoutingType);
            if (messageType === Messages.PUR) document.querySelector(".wmeSamMessage.pur").addEventListener("click", () => { document.querySelector(".venue-alerts wz-alert span[slot=action]").click(); });
        });

        if (W.snapshotManager.isSnapshotOn()) messageContainer.addMessage(Messages.SNAPSHOT_ACTIVE);
        if (!document.querySelector(".wmeSamMessage.ea") && [...selected].some(obj => isOOEA(obj.id))) messageContainer.addMessage(Messages.EA);
        if (!!maxRoutingType) {
            messageContainer.addMessage(Messages.ROUTING_PREFERENCE);
            addLockRequestCopier(maxLockRank + 1, !!maxRoutingType);
        }

        // hide message box if no unhandled alerts
        const wzAlertsGroup = document.querySelector("wz-alerts-group");
        const visibleAlerts = Array.from(wzAlertsGroup?.children || []).filter(_ => _?.classList.contains('hide') === false);
        if (visibleAlerts.length === 0) {
            wzAlertsGroup.classList.add('hide');
        }
    }

    function initialize() {
        console.log("WME SAM: Loaded");
        W.selectionManager.events.register('selectionchanged', this, processSelectionChange);

        // run preselected features from url
        if (W.selectionManager.getSelectedFeatures().length > 0) processSelectionChange();

        // add styling
        let style = document.createElement("style");
        style.innerHTML = policy.createHTML(`/* WME Simple Alert Messages Styling */
            #wmeSamContainer {padding-top: 10px;}
            .wmeSamMessage {padding: 7px 10px; border-radius: 10px; width: fit-content; margin-left: 5px; white-space: nowrap;}
            .wmeSamMessage .w-icon {font-size: 20px;vertical-align: middle;}
            .wmeSamMessage.locked {background-color: #FE5F5D; cursor: pointer;}
            .wmeSamMessage.ea, .wmeSamMessage.junction_box {background-color: #FF9800;}
            .wmeSamMessage.mixed, .wmeSamMessage.snapshot {background-color: #42A5F5;}
            .wmeSamMessage.pur {background-color: #C9B5FF; cursor: pointer;}
            .wmeSamMessage.closure {background-color: #FE5F5D; cursor: pointer;}
            .wmeSamMessage.routing {background-color: #FF9800; cursor: pointer;}
            .wmeSamAdvancedErrorDetails { width: 343px; margin: auto; padding: 10px 0; }`);
        document.body.appendChild(style);
    }

    // bootstrap
    W?.userscripts?.state?.isReady ? initialize() : document.addEventListener("wme-ready", initialize, { once: true });


    // override fetch api to intercept errors
    const { fetch: originalFetch } = window;
    window.fetch = async (...args) => {
        let [resource, config] = args;
        let response = await originalFetch(resource, config);
        if (!response.ok) {
            window.errorObject = response;
            addAdvancedErrorDetails(response.clone());
        }
        return response;
    };
})();