Speed Limits Viewer

A visualizer for the GPS Speed Limits Logger app

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Speed Limits Viewer
// @license MIT
// @namespace    https://shryder.me/
// @version      2025-07-07
// @description  A visualizer for the GPS Speed Limits Logger app
// @author       Shryder
// @include         /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @icon            https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @require         https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require         https://update.greasyfork.org/scripts/509664/1461898/WME%20Utils%20-%20Bootstrap.js
// @require         https://greasyfork.org/scripts/389765-common-utils/code/CommonUtils.js?version=1090053
// @require         https://greasyfork.org/scripts/450160-wme-bootstrap/code/WME-Bootstrap.js?version=1090054
// @require         https://greasyfork.org/scripts/452563-wme/code/WME.js?version=1101598
// @require         https://greasyfork.org/scripts/450221-wme-base/code/WME-Base.js?version=1101617
// @require         https://greasyfork.org/scripts/450320-wme-ui/code/WME-UI.js?version=1101616
// @grant           GM_xmlhttpRequest
// ==/UserScript==

var SpeedLimitReports_Layer;
const icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIGZpbGw9IiMwMDdiZmYiIHN0cm9rZT0iIzAwNTZiMyIgc3Ryb2tlLXdpZHRoPSIyIi8+PHRleHQgeD0iMjAiIHk9IjI1IiBmb250LXNpemU9IjIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4xPC90ZXh0Pjwvc3ZnPg==';

const speedLimitIcon = `
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <circle cx="50" cy="50" r="48" fill="white" stroke="red" stroke-width="4"/>
  <text x="50" y="60" text-anchor="middle" font-size="36" font-family="Arial, sans-serif" fill="black">##speedlimit##</text>
</svg>
`;

const icon0 = `
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
  <circle cx="20" cy="20" r="18" fill="#007bff" stroke="#0056b3" stroke-width="2"/>
  <text x="20" y="25" font-size="20" font-family="Arial, sans-serif" fill="white" text-anchor="middle">1</text>
</svg>
`;

const icon1 = `
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
  <circle cx="20" cy="20" r="18" fill="#007bff" stroke="#0056b3" stroke-width="2"/>
  <text x="20" y="25" font-size="20" font-family="Arial, sans-serif" fill="white" text-anchor="middle">2</text>
</svg>
`;

const icon2 = `
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
  <circle cx="20" cy="20" r="18" fill="#007bff" stroke="#0056b3" stroke-width="2"/>
  <text x="20" y="25" font-size="20" font-family="Arial, sans-serif" fill="white" text-anchor="middle">3</text>
</svg>
`;
(async function() {
    'use strict';

    let helper, tab, uuidInput;

    let sdk = await bootstrap();

    function setupTab() {
        const BUTTONS = {
            A: {
                title: "Fetch",
                description: "Desc",
                callback: function() {
                    console.log('[GPSSpeedLimitsLogger] Fetching...');
                    let uuidInput = document.getElementById("speed-limits-reports-viewer-speed-limit-log-id");

                    fetchReports(uuidInput.value);

                    return false;
                }
            },
            ClearUI: {
                title: "Clear",
                description: "Clear Speed Limit logger UI elements",
                callback: function() {
                    SpeedLimitReports_Layer.removeAllFeatures();

                    return false;
                }
            },
        };

        console.log("Hello", sdk.State.getUserInfo().userName, WazeWrap);

        let helper = new WMEUIHelper("Speed Limits Reports Viewer");
        let tab = helper.createTab("Speed Limit Reports");
        tab.addButtons(BUTTONS);
        tab.addInput("speed-limit-log-id", "UUID to Fetch", "", () => {}, "30-06-2025-19_21_32___bce7c826-13bf-425d-8890-9254f5b3d7ee");

        tab.inject();
    }

    function buildReport(report) {
        let style = {
            externalGraphic: icon,
            graphicWidth: 32,
            graphicHeight: 38,
            graphicYOffset: -42,
            fillOpacity: 1,
            title: 'LiveMap',
            cursor: 'help'
        };

        const reportLocation_icon = "data:image/svg+xml;base64," + btoa(speedLimitIcon.replaceAll("##speedlimit##", report.speedLimit));
        let coords = OpenLayers.Layer.SphericalMercator.forwardMercator(report.gpsLocation.long, report.gpsLocation.lat);
        let reportLocation = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(coords.lon,coords.lat), { speedLimit: report.speedLimit }, { ...style, externalGraphic: reportLocation_icon });

        /*
        TODO
        const prev0_icon = "data:image/svg+xml;base64," + btoa(icon0);
        let prev0_location = report.previousGpsLocations[0];
        let prev0_coords = OpenLayers.Layer.SphericalMercator.forwardMercator(prev0_location.long, prev0_location.lat);
        let prev0 = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(prev0_coords.lon,prev0_coords.lat), { speedLimit: 0 }, { ...style, externalGraphic: prev0_icon })

        const prev1_icon = "data:image/svg+xml;base64," + btoa(icon1);
        let prev1_location = report.previousGpsLocations[1];
        let prev1_coords = OpenLayers.Layer.SphericalMercator.forwardMercator(prev1_location.long, prev1_location.lat);
        let prev1 = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(prev1_coords.lon,prev1_coords.lat), { speedLimit: 0 }, { ...style, externalGraphic: prev1_icon })

        const prev2_icon = "data:image/svg+xml;base64," + btoa(icon2);
        let prev2_location = report.previousGpsLocations[2];
        let prev2_coords = OpenLayers.Layer.SphericalMercator.forwardMercator(prev2_location.long, prev2_location.lat);
        let prev2 = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(prev2_coords.lon,prev2_coords.lat), { speedLimit: 0 }, { ...style, externalGraphic: prev2_icon })
        */
        return [ reportLocation ];
    }

    function fetchReports(filename) {
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://speedlimits.shryder.me/api/logs/${filename}.json`,
        onload: function(response){
            if (response.status < 200 || response.status > 300) {
                console.log("[GPSSpeedLimitsLogger] Error fetching speed limit logs", response);
                return;
            }

            let response_json = JSON.parse(response.response);
            console.log("[GPSSpeedLimitsLogger] Loaded report logs:", response, response_json);

            if (!("success" in response_json) || !response_json.success) {
                console.log("[GPSSpeedLimitsLogger] Error fetching", response);
                return;
            }

            SpeedLimitReports_Layer.removeAllFeatures();
            for (let i = 0; i < response_json.data.length; i++) {
                SpeedLimitReports_Layer.addFeatures(buildReport(response_json.data[i]));
            }

            console.log("[GPSSpeedLimitsLogger] Finished loading reports into layer");
            SpeedLimitReports_Layer.redraw();
        },
        onerror: (error) => {
            console.log("[GPSSpeedLimitsLogger] Error loading speed limit reports", error);
        }
      });
    }

    function main() {
        setupTab();

        // TODO: titles and labels
        let styleMap = new OpenLayers.StyleMap({
            "default": new OpenLayers.Style({
                // fillColor: '${fillColor}',
                // fillOpacity: '${opacity}',
                fontColor: '#111111',
                fontWeight: 'bold',
                strokeColor: '#ffffff',
                // strokeOpacity: '${opacity}',
                strokeWidth: 2,
                // pointRadius: '${radius}',
                label: '${title}',
                title: '${title}'
            }, {
                context: {
                    title: (feature) => {
                        if (feature.attributes && feature.attributes.speedLimit) return `${feature.speedLimit} km/h`;

                        return "N/A";
                    }
                }
            })
        });

        SpeedLimitReports_Layer = new OpenLayers.Layer.Vector("Speed Limit Reports", {
            uniqueName: "__speedlimit_reports",
            styleMap: styleMap
        });

        SpeedLimitReports_Layer.setVisibility(true);

        W.map.addLayer(SpeedLimitReports_Layer);

        console.log("[GPSSpeedLimitsLogger] Ready.");
    }

    main();
})();