// ==UserScript==
// @name Internet Roadtrip Speedometer
// @namespace spideramn.github.io
// @match https://neal.fun/internet-roadtrip/*
// @version 0.0.3
// @author Spideramn
// @description Internet Roadtrip Speedometer.
// @license MIT
// @grant GM.addStyle
// @grant GM.setValues
// @grant GM.getValues
// @run-at document-start
// @icon https://neal.fun/favicons/internet-roadtrip.png
// @require https://cdn.jsdelivr.net/npm/[email protected]
// @require https://cdnjs.cloudflare.com/ajax/libs/gauge.js/1.3.9/gauge.min.js
// ==/UserScript==
// This works together with irf.d.ts to give us type hints
/* globals IRF Gauge */
/**
* Internet Roadtrip Framework
* @typedef {typeof import('internet-roadtrip-framework')} IRF
*/
(async function() {
'use strict';
if (!IRF?.isInternetRoadtrip) {
return;
}
// Get map methods and various objects
const odometer = await IRF.vdom.odometer;
const wheelDom = await IRF.dom.wheel;
// Speedometer
class SpeedOmeterControl
{
_queue = [];
_speedOmeterContainer = undefined;
_speedOmeterElement = undefined;
_gauge = undefined;
addDistance(distanceInMiles)
{
// prevent duplicate distances
if(this._queue.length > 0)
{
const lastDistance = this._queue[this._queue.length - 1].distance;
if(distanceInMiles == lastDistance)
{
this.update();
return;
}
}
this._queue.push({time: Date.now(), distance: distanceInMiles});
if (this._queue.length > 15)
{
this._queue.shift();
}
this.update();
};
async setup()
{
GM.addStyle(`
.speedOmeterContainer {
width: 300px; // 70%;
height: 100px; // 25%;
position: absolute;
top: 30px; // 10%;
left: 35px; // 15%;
visibility: hidden;
}
.speedOmeterContainer canvas{
position: absolute;
width: 100%;
height: 100%;
}
.speedOmeterContainer span {
position: absolute;
width: 100%;
bottom: 0%;
left: 0%;
height: 50%;
text-align: center;
font-family: Roboto,sans-serif;
color: #1F1F1F;
font-size: 17px;
}
`);
// add dom elements
this._speedOmeterContainer = document.createElement('div');
this._speedOmeterContainer.className = "speedOmeterContainer";
const speedOmeterCanvas = document.createElement('canvas');
this._speedOmeterElement = document.createElement('span');
this._speedOmeterContainer.appendChild(speedOmeterCanvas);
this._speedOmeterContainer.appendChild(this._speedOmeterElement);
wheelDom.prepend(this._speedOmeterContainer);
var opts = {
angle: 0,
lineWidth: 1,
radiusScale: 1,
pointer: {
length: 1,
strokeWidth: 0.035,
color: '#AA000099'
},
limitMax: true,
limitMin: true,
generateGradient: false,
highDpiSupport: true,
colorStart: '#FFFFFF99',
colorStop: '#FFFFFF99',
strokeColor: '#FFFFFF99',
// renderTicks is Optional
renderTicks: {
divisions: 6,
divWidth: 2,
divLength: 0.1,
divColor: '#333333',
subDivisions: 5,
subLength: 0.05,
subWidth: 1,
subColor: '#666666'
},
staticLabels: {
font: "10px Roboto,sans-serif",
labels: [0, 5, 10, 15, 20, 25, 30],
color: "#000000",
fractionDigits: 0
}
};
this._gauge = new Gauge(speedOmeterCanvas).setOptions(opts);
this._gauge.maxValue = 30;
this._gauge.set(0); // set actual value
// set hook
odometer.state.updateDisplay = new Proxy(odometer.methods.updateDisplay, {
apply: (target, thisArg, args) => {
this.addDistance(odometer.props.miles);
return Reflect.apply(target, thisArg, args);;
},
});
// update position of speedometer
this.updatePosition();
}
update()
{
if(settings.speedometer_enabled)
{
this._speedOmeterContainer.style.visibility = 'visible';
if(this._queue.length < 3)
{
this._gauge.set(0);
this._speedOmeterElement.innerText = 'Calibrating...';
return;
}
const lastItem = this._queue[this._queue.length - 1];
const firstItem = this._queue[0];
const timeInHours = (lastItem.time - firstItem.time) / (1000 * 60 * 60);
let speed = (lastItem.distance - firstItem.distance) / timeInHours; // in mph
if(odometer.data.isKilometers)
{
speed *= odometer.data.conversionFactor;
}
this._gauge.set(speed);
this._speedOmeterElement.innerText = (speed.toFixed(1) + (odometer.data.isKilometers? ' km/h' : ' mph'));
}
else
{
this._gauge.set(0);
this._speedOmeterElement.innerText = '';
this._speedOmeterContainer.style.visibility = 'hidden';
}
}
updatePosition()
{
if(this._speedOmeterContainer)
{
this._speedOmeterContainer.style.left = settings.speedometer_left + 'px';
this._speedOmeterContainer.style.top = settings.speedometer_top + 'px';
this._speedOmeterContainer.style.transform = 'scale(' + settings.speedometer_scale + ')';
}
}
};
//
// Settings
const settings = {
"speedometer_enabled": true,
"speedometer_scale": 1.0,
"speedometer_left": 35,
"speedometer_top": 30,
};
const storedSettings = await GM.getValues(Object.keys(settings))
Object.assign(settings, storedSettings);
await GM.setValues(settings);
// settings panel
let gm_info = GM.info
gm_info.script.name = "Speedometer"
const irf_settings = IRF.ui.panel.createTabFor(gm_info, { tabName: "Speedometer" });
add_checkbox('Display speedometer', 'speedometer_enabled', () => speedOmeter.update());
const header = document.createElement('h3');
header.innerText = 'Position';
irf_settings.container.appendChild(header);
let button = document.createElement('button');
button.innerText = 'Reset position';
button.addEventListener('click', () => {
speedometer_left_element.value = settings.speedometer_left = 35;
speedometer_top_element.value = settings.speedometer_top = 30;
speedometer_scale_element.value = settings.speedometer_scale = 1.0;
GM.setValues(settings);
speedOmeter.updatePosition();
});
irf_settings.container.appendChild(button);
irf_settings.container.appendChild(document.createElement('br'));
const speedometer_left_element = add_numeric('Left', 'speedometer_left', -1000, 1000, 1, () => speedOmeter.updatePosition());
const speedometer_top_element = add_numeric('Top', 'speedometer_top', -1000, 1000, 1, () => speedOmeter.updatePosition());
const speedometer_scale_element = add_numeric('Scale', 'speedometer_scale', 0.5, 2, 0.1, () => speedOmeter.updatePosition());
function add_checkbox(name, identifier, callback=undefined, settings_container=irf_settings.container) {
let label = document.createElement("label");
let checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = settings[identifier];
checkbox.className = IRF.ui.panel.styles.toggle;
label.appendChild(checkbox);
let text = document.createElement("span");
text.innerText = " " + name;
label.appendChild(text);
checkbox.addEventListener("change", () => {
settings[identifier] = checkbox.checked;
GM.setValues(settings);
if (callback) callback(checkbox.checked);
});
settings_container.appendChild(label);
settings_container.appendChild(document.createElement("br"));
settings_container.appendChild(document.createElement("br"));
return checkbox
};
function add_numeric(name, identifier, min, max, step, callback=undefined, settings_container=irf_settings.container) {
let label = document.createElement("label");
label.style.display = 'block';
let text = document.createElement("span");
text.innerText = " " + name;
label.appendChild(text);
label.appendChild(document.createElement("br"));
let input = document.createElement("input");
input.id = "speedometer_" + identifier;
input.type = "number";
input.value = settings[identifier];
input.min = min;
input.max = max;
input.step = step;
input.className = IRF.ui.panel.styles.input;
label.appendChild(input);
input.addEventListener("change", () => {
settings[identifier] = parseFloat(input.value);
GM.setValues(settings);
if (callback) callback(parseFloat(input.value));
});
settings_container.appendChild(label);
return input;
};
const speedOmeter = new SpeedOmeterControl();
await speedOmeter.setup();
})();