// ==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();
})();