WME Open GMaps Editor

Opens the Google Maps editor based on WME's map center.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME Open GMaps Editor
// @namespace    https://github.com/WazeDev/wme-open-gmaps-editor
// @version      0.0.6
// @description  Opens the Google Maps editor based on WME's map center.
// @author       Gavin Canon-Phratsachack (https://github.com/gncnpk)
// @match        https://beta.waze.com/*editor*
// @match        https://www.waze.com/*editor*
// @exclude      https://www.waze.com/*user/*editor/*
// @exclude      https://www.waze.com/discuss/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=waze.com
// @license      MIT
// @contributionURL https://github.com/WazeDev/Thank-The-Authors
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    let wmeSdk;
    const zoomLevels = {
        1: 52428800,
        2: 26214400,
        3: 13107200,
        4: 6553600,
        5: 3276800,
        6: 1638400,
        7: 819200,
        8: 409600,
        9: 204800,
        10: 102400,
        11: 51200,
        12: 25600,
        13: 12800,
        14: 6400,
        15: 3200,
        16: 1600,
        17: 800,
        18: 400,
        19: 200,
        20: 100,
        21: 50,
        22: 25
    }

    function encodeProtobufToBase64({
        latitude,
        longitude,
        zoomLevel,
        sequenceNumber,
        url
    } = {}) {

        // Helper function to encode varint - EXACT ORIGINAL
        function encodeVarint(value) {
            const bytes = [];
            while (value >= 0x80) {
                bytes.push((value & 0xFF) | 0x80);
                value >>>= 7;
            }
            bytes.push(value & 0xFF);
            return new Uint8Array(bytes);
        }

        // Helper function to encode 64-bit fixed (double)
        function encodeFixed64(value) {
            const buffer = new ArrayBuffer(8);
            const view = new DataView(buffer);
            view.setFloat64(0, value, true); // little endian
            return new Uint8Array(buffer);
        }

        // Helper function to encode 32-bit fixed (uint32)
        function encodeFixed32(value) {
            const buffer = new ArrayBuffer(4);
            const view = new DataView(buffer);
            view.setUint32(0, value, true); // little endian
            return new Uint8Array(buffer);
        }

        // Helper function to encode length-delimited data
        function encodeLengthDelimited(data) {
            const length = encodeVarint(data.length);
            const result = new Uint8Array(length.length + data.length);
            result.set(length, 0);
            result.set(data, length.length);
            return result;
        }

        // Helper function to encode string
        function encodeString(str) {
            const encoder = new TextEncoder();
            const data = encoder.encode(str);
            return encodeLengthDelimited(data);
        }

        // Helper function to encode field header (field number + wire type)
        function encodeFieldHeader(fieldNumber, wireType) {
            return encodeVarint((fieldNumber << 3) | wireType);
        }

        // Helper function to concatenate Uint8Arrays
        function concatUint8Arrays(...arrays) {
            const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
            const result = new Uint8Array(totalLength);
            let offset = 0;
            for (const arr of arrays) {
                result.set(arr, offset);
                offset += arr.length;
            }
            return result;
        }

        // Wire types
        const VARINT = 0;
        const FIXED64 = 1;
        const LENGTH_DELIMITED = 2;
        const FIXED32 = 5;

        try {
            // Encode the inner coordinate message (field 1.1)
            const coordMessage = concatUint8Arrays(
                encodeFieldHeader(2, FIXED64), encodeFixed64(longitude),
                encodeFieldHeader(3, FIXED64), encodeFixed64(latitude)
            );

            // Encode the dimensions message (field 1.3)
            const dimensionsMessage = concatUint8Arrays(
                encodeFieldHeader(1, VARINT), encodeVarint(1920),
                encodeFieldHeader(2, VARINT), encodeVarint(957)
            );

            // Encode the main nested message (field 1)
            const mainMessage = concatUint8Arrays(
                encodeFieldHeader(1, LENGTH_DELIMITED), encodeLengthDelimited(coordMessage),
                encodeFieldHeader(3, LENGTH_DELIMITED), encodeLengthDelimited(dimensionsMessage),
                encodeFieldHeader(4, FIXED32), encodeFixed32(zoomLevels[zoomLevel])
            );

            // Encode the complete protobuf message
            const completeMessage = concatUint8Arrays(
                encodeFieldHeader(1, LENGTH_DELIMITED), encodeLengthDelimited(mainMessage),
                encodeFieldHeader(2, VARINT), encodeVarint(sequenceNumber),
                encodeFieldHeader(4, LENGTH_DELIMITED), encodeString(url)
            );

            // Convert to URL-safe Base64
            const base64String = btoa(String.fromCharCode(...completeMessage))
                .replace(/\+/g, '-')
                .replace(/\//g, '_')
                .replace(/=/g, '');

            return base64String;
        } catch (error) {
            console.error('Error encoding protobuf:', error);
            return null;
        }
    }

    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    window.SDK_INITIALIZED.then(initialize)

    async function initialize() {
        wmeSdk = await getWmeSdk({
            scriptId: 'wme-open-gmaps-editor',
            scriptName: 'WME Open GMaps Editor'
        })

        wmeSdk.Events.on({
            eventHandler: replaceGoogleLink,
            eventName: "wme-map-move-end"
        })

        wmeSdk.Events.on({
            eventHandler: replaceGoogleLink,
            eventName: "wme-map-zoom-changed"
        })

        let GMELink = document.createElement("a");
        GMELink.id = "wme-open-gmaps-editor-url";
        GMELink.target = "_blank";
        let img = document.createElement("img");
        img.src = "https://upload.wikimedia.org/wikipedia/commons/a/aa/Google_Maps_icon_%282020%29.svg";
        img.style = "margin-right: 12px;margin-left: 12px;width: 16px;";
        img.title = "Open in Google Maps Editor (GME)"
        GMELink.appendChild(img);

        waitForElm('.secondary-toolbar-actions').then((elm) => {
            let toolbar = document.getElementsByClassName("secondary-toolbar-actions")[0];
            toolbar.insertBefore(GMELink, toolbar.children[0]);
            replaceGoogleLink();
        });
    }

    function replaceGoogleLink() {
        const coords = wmeSdk.Map.getMapCenter();
        const mapZoom = wmeSdk.Map.getZoomLevel();

        const trafficMapLayer_DO = "!5m1!1e1"
        const mapFixDialog_DO = "!10m1!1e3"
        let mapUrl = `https://www.google.com/maps/place/@${coords.lat},${coords.lon},${zoomLevels[mapZoom]}m/data=${trafficMapLayer_DO}${mapFixDialog_DO}`

        const base64Data = encodeProtobufToBase64({
            latitude: coords.lat,
            longitude: coords.lon,
            zoomLevel: mapZoom,
            sequenceNumber: 10,
            url: mapUrl
        });

        if (base64Data) {
            let url = `https://maps.google.com/roadeditor/iframe?bpb=${base64Data}`
            document.getElementById("wme-open-gmaps-editor-url").href = url;
        } else {
            console.error('Failed to encode protobuf data');
        }
    }
})();