// ==UserScript==
// @name Internet Roadtrip Minimap tricks
// @namespace jdranczewski.github.io
// @description Provide many bonus options for the Internet Roadtrip minimap.
// @match https://neal.fun/internet-roadtrip/*
// @icon https://files.catbox.moe/v4yu3f.png
// @version 0.7.1
// @author jdranczewski (+netux +GameRoMan)
// @license MIT
// @require https://cdn.jsdelivr.net/npm/[email protected]
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2/dist/solid.min.js
// @require https://cdn.jsdelivr.net/gh/ianengelbrecht/geo-coordinates-parser@b06d051f2a70bc95c2fa1a063ceef85f19823fee/bundle/geocoordsparser.js
// @require https://cdn.jsdelivr.net/npm/@turf/[email protected]/turf.min.js
// @grant GM.addStyle
// @grant GM.getValues
// @grant GM.info
// @grant GM.setValues
// @grant GM_getValue
// @grant unsafeWindow
// ==/UserScript==
const geoCoordinatesParser = {convert};
(async function (web, IRF, solidJs, geoCoordinatesParser, turf) {
'use strict';
if (!IRF.isInternetRoadtrip) return;
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var IRF__namespace = /*#__PURE__*/_interopNamespaceDefault(IRF);
var css_248z = "@media (max-width:768px){body{height:auto!important}}@media (min-width:900px){.map-container .expand-button{cursor:nesw-resize;display:flex!important}.map-container #mini-map{height:var(--map-height,170px)!important;position:relative;width:var(--map-width,250px)!important}.expanded #mini-map{height:var(--map-height-expanded,300px)!important;width:var(--map-width-expanded,450px)!important}.expanded .expand-button img{rotate:180deg}.expanded{opacity:var(--map-opacity-expanded,1)!important}}.map-container{transition:opacity .5s}.map-container:hover{opacity:1!important}.map-container{z-index:101!important}.map-container.fullscreen{bottom:0;height:100%;left:0;opacity:1!important;top:0;transform:none;width:100%;z-index:-1!important}.map-container.fullscreen #mini-map{border-radius:0;height:100%!important;width:100%!important}.map-container.fullscreen .expand-button{display:none!important}.map-container.fullscreen .maplibregl-ctrl-top-left{margin-left:2px;margin-top:58px}.map-container.fullscreen .maplibregl-ctrl-bottom-right{margin-bottom:186px}.map-container.fullscreen .maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale{margin-right:11px!important}.lotwv1-overlay:has(~.fullscreen){display:none!important}.maplibregl-marker{align-items:center;display:flex;justify-content:center;opacity:var(--marker-opacity,1)!important}.mmt-map-menu-opened{opacity:1!important}#mini-map #mmt-menu{position:fixed;transform:translateY(-100%);z-index:1000}#mini-map #mmt-menu button{align-items:center;display:flex;text-align:left;width:100%}#mini-map #mmt-menu .maplibregl-ctrl-icon{width:29px}#mini-map #mmt-menu .maplibregl-ctrl-icon+span{align-items:center;display:flex;margin:0 9px 0 5px}#mini-map #mmt-menu #mmt-menu-label{background:#f1f1f1;display:flex;font-size:14px;justify-content:space-between;margin:5px 0 0;padding:6px}#mini-map #mmt-menu #mmt-menu-label #mmt-menu-close{cursor:pointer;margin-left:10px;margin-right:2px}#mini-map #mmt-menu .mmt-draggable-checkbox-icon{align-items:center;display:flex;justify-content:center}#mini-map #mmt-menu .mmt-draggable-checkbox-icon input{height:50%;pointer-events:none;width:50%}#mini-map #mmt-menu #mmt-menu-color{display:none}#mini-map .mmt-menu-Car .mmt-hide-Car,#mini-map .mmt-menu-Map .mmt-hide-Map,#mini-map .mmt-menu-Marker .mmt-hide-Marker{display:none!important}#mini-map .mmt-menu-Car .mmt-hide-Car,#mini-map .mmt-menu-Map .mmt-hide-Map,#mini-map .mmt-menu-Marker .mmt-hide-Marker{opacity:.5!important}#mini-map .mmt-distance-control{display:flex}#mini-map .mmt-distance-control div{align-items:center;border-right:1px solid #ddd;display:flex;padding:0 5px}#mini-map .maplibregl-ctrl-compass{cursor:pointer}.mmt-miles-decimal .miles-text{line-height:10px;text-align:center}.mmt-miles-decimal .miles-text span{display:inline!important;font-size:10px}@media (display-mode:picture-in-picture){.maplibregl-ctrl-scale{margin:0 5px 5px 0!important}#mini-map #mmt-menu{left:auto!important;right:10px!important;top:10px!important;transform:translate(0)!important}}";
var styles = {"settings-section":"settings-module_settings-section__RSYGf","settings-item-margin":"settings-module_settings-item-margin__l4XUx","settings-item":"settings-module_settings-item__Arp6P","inverse":"settings-module_inverse__8evpX","setting":"settings-module_setting__qVTOm"};
var stylesheet="a{color:#aaa}.settings-module_settings-section__RSYGf>hr{display:none}.settings-module_settings-section__RSYGf~.settings-module_settings-section__RSYGf>hr{display:block;margin-top:1.5rem}.settings-module_settings-section__RSYGf p{text-align:justify}.settings-module_settings-item-margin__l4XUx{display:grid;margin-right:.5rem}.settings-module_settings-item__Arp6P{align-items:center;display:grid;grid-template-columns:auto 1fr auto;margin-right:.5rem}.settings-module_settings-item__Arp6P.settings-module_inverse__8evpX{grid-template-columns:1fr auto 1fr;margin-left:.75rem;margin-right:1.25rem}.settings-module_settings-item__Arp6P>hr{--un-border-opacity:1;background-color:initial;border-color:rgb(65 71 75/var(--un-border-opacity));border-style:dashed;border-width:1px;color:transparent;width:100%}.settings-module_setting__qVTOm{margin:.5rem .75rem}";
var _tmpl$ = /*#__PURE__*/web.template(`<hr>`),
_tmpl$2 = /*#__PURE__*/web.template(`<h2>`),
_tmpl$3 = /*#__PURE__*/web.template(`<p>`),
_tmpl$4 = /*#__PURE__*/web.template(`<div><span></span><hr><div><input type=checkbox>`),
_tmpl$5 = /*#__PURE__*/web.template(`<div><div><label> <!>: </label><input type=range>`),
_tmpl$6 = /*#__PURE__*/web.template(`<div><hr><div><button></button></div><hr>`),
_tmpl$7 = /*#__PURE__*/web.template(`<button>Reset`),
_tmpl$8 = /*#__PURE__*/web.template(`<div><span>:</span><input style=width:100%>`),
_tmpl$9 = /*#__PURE__*/web.template(`<div><p>`);
// Default settings
const settings = {
"expand_map": false,
"default_zoom": 12.5,
"timeout_centre": true,
"timeout_centre_fullscreen_disable": false,
"reset_zoom": false,
"align_orientation": false,
"show_scale": true,
"km_units": false,
"decimal_units": false,
"coordinates_fancy": false,
"map_size": {
width: undefined,
height: undefined,
expanded_width: undefined,
expanded_height: undefined
},
"map_opacity": "1",
"background_opacity": "1",
"map_opacity_expanded": "1",
"background_opacity_expanded": "1",
"marker_opacity": "1",
"route_opacity": "1",
"marker_color": "#f7a000",
"markers": {},
"draggable_markers": true,
"car_marker_custom": false,
"car_marker_size": 54,
"car_marker_url": "https://files.catbox.moe/a55qk5.png",
"car_marker_scale": 65,
"car_marker_rotation": 90,
"car_marker_flip": false,
"car_marker_flip_x": false,
"side_compass": false,
"coverage": true,
"coverage_opacity": "0.75"
};
// Initialise settings
const storedSettings = await( GM.getValues(Object.keys(settings)));
Object.assign(settings, storedSettings);
// Migrate stored markers to the new format
Object.entries(settings.markers).forEach(([key, value]) => {
if (Array.isArray(value) && value.length == 2) {
settings.markers[key] = [value[0], value[1], {}];
} else if (typeof value[2] === 'string' || value[2] instanceof String) {
settings.markers[key] = [value[0], value[1], {
color: value[2]
}];
}
});
GM.setValues(settings);
// Update script name so it takes up less space
const gm_info = GM.info;
gm_info.script.name = "Minimap tricks";
// Wrapper around IRF panel
class Section {
constructor(name, description) {
this.name = name;
this.description = description;
this.container = document.createElement("div");
this.render_header();
}
render_header() {
this.container.classList.add(styles['settings-section']);
const _self$ = this;
const item = [_tmpl$(), (() => {
var _el$2 = _tmpl$2();
web.insert(_el$2, () => _self$.name);
return _el$2;
})(), web.createComponent(web.Show, {
get when() {
return _self$.description;
},
get children() {
var _el$3 = _tmpl$3();
web.effect(() => _el$3.innerHTML = _self$.description);
return _el$3;
}
})];
web.render(() => item, this.container);
}
add_checkbox(name, identifier, callback = undefined) {
const item = (() => {
var _el$4 = _tmpl$4(),
_el$5 = _el$4.firstChild,
_el$6 = _el$5.nextSibling,
_el$7 = _el$6.nextSibling,
_el$8 = _el$7.firstChild;
web.insert(_el$5, name);
_el$8.addEventListener("change", e => {
settings[identifier] = e.currentTarget.checked;
GM.setValues(settings);
if (callback) callback(e.currentTarget.checked);
});
web.effect(_p$ => {
var _v$ = styles['settings-item'],
_v$2 = styles['setting'],
_v$3 = styles['setting'],
_v$4 = IRF__namespace.ui.panel.styles.toggle;
_v$ !== _p$.e && web.className(_el$4, _p$.e = _v$);
_v$2 !== _p$.t && web.className(_el$5, _p$.t = _v$2);
_v$3 !== _p$.a && web.className(_el$7, _p$.a = _v$3);
_v$4 !== _p$.o && web.className(_el$8, _p$.o = _v$4);
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined
});
web.effect(() => _el$8.checked = settings[identifier]);
return _el$4;
})();
web.render(() => item, this.container);
}
add_slider(name, identifier, callback = undefined, slider_bits = [1, 17, .5]) {
const [value, setValue] = solidJs.createSignal(settings[identifier]);
solidJs.createEffect(solidJs.on(value, () => {
settings[identifier] = value();
GM.setValues(settings);
if (callback) callback(value());
}, {
defer: true
}));
const item = (() => {
var _el$9 = _tmpl$5(),
_el$0 = _el$9.firstChild,
_el$1 = _el$0.firstChild,
_el$10 = _el$1.firstChild,
_el$12 = _el$10.nextSibling;
_el$12.nextSibling;
var _el$13 = _el$1.nextSibling;
web.insert(_el$1, name, _el$12);
web.insert(_el$1, value, null);
_el$13.$$input = e => setValue(e.target.value);
web.effect(_p$ => {
var _v$5 = styles['settings-item-margin'],
_v$6 = styles['setting'],
_v$7 = slider_bits[0],
_v$8 = slider_bits[1],
_v$9 = slider_bits[2],
_v$0 = IRF__namespace.ui.panel.styles.slider;
_v$5 !== _p$.e && web.className(_el$9, _p$.e = _v$5);
_v$6 !== _p$.t && web.className(_el$0, _p$.t = _v$6);
_v$7 !== _p$.a && web.setAttribute(_el$13, "min", _p$.a = _v$7);
_v$8 !== _p$.o && web.setAttribute(_el$13, "max", _p$.o = _v$8);
_v$9 !== _p$.i && web.setAttribute(_el$13, "step", _p$.i = _v$9);
_v$0 !== _p$.n && web.className(_el$13, _p$.n = _v$0);
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined
});
web.effect(() => _el$13.value = value());
return _el$9;
})();
web.render(() => item, this.container);
}
add_button(name, callback) {
const item = (() => {
var _el$14 = _tmpl$6(),
_el$15 = _el$14.firstChild,
_el$16 = _el$15.nextSibling,
_el$17 = _el$16.firstChild;
_el$17.$$click = () => callback();
web.insert(_el$17, name);
web.effect(_p$ => {
var _v$1 = [styles['settings-item'], styles['inverse']].join(' '),
_v$10 = styles['setting'];
_v$1 !== _p$.e && web.className(_el$14, _p$.e = _v$1);
_v$10 !== _p$.t && web.className(_el$16, _p$.t = _v$10);
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$14;
})();
web.render(() => item, this.container);
}
add_input(name, identifier, type, callback, default_value) {
const [value, setValue] = solidJs.createSignal(settings[identifier]);
// We use on with defer here so the effect only runs when value changes
// and not when the effect is initially created
solidJs.createEffect(solidJs.on(value, () => {
settings[identifier] = value();
GM.setValues(settings);
if (callback) callback(value());
}, {
defer: true
}));
const item = (() => {
var _el$18 = _tmpl$8(),
_el$19 = _el$18.firstChild,
_el$20 = _el$19.firstChild,
_el$21 = _el$19.nextSibling;
web.insert(_el$19, name, _el$20);
_el$21.addEventListener("change", e => setValue(e.target.value));
web.setAttribute(_el$21, "type", type);
web.insert(_el$18, web.createComponent(web.Show, {
when: default_value,
get children() {
var _el$22 = _tmpl$7();
_el$22.$$click = () => setValue(default_value);
web.effect(() => web.className(_el$22, styles['setting']));
return _el$22;
}
}), null);
web.effect(_p$ => {
var _v$11 = styles['settings-item'],
_v$12 = styles['setting'];
_v$11 !== _p$.e && web.className(_el$18, _p$.e = _v$11);
_v$12 !== _p$.t && web.className(_el$19, _p$.t = _v$12);
return _p$;
}, {
e: undefined,
t: undefined
});
web.effect(() => _el$21.value = value());
return _el$18;
})();
web.render(() => item, this.container);
}
add_comment(text) {
const item = (() => {
var _el$23 = _tmpl$9(),
_el$24 = _el$23.firstChild;
_el$24.innerHTML = text;
web.effect(_p$ => {
var _v$13 = styles['settings-item-margin'],
_v$14 = styles['setting'];
_v$13 !== _p$.e && web.className(_el$23, _p$.e = _v$13);
_v$14 !== _p$.t && web.className(_el$24, _p$.t = _v$14);
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$23;
})();
web.render(() => item, this.container);
}
}
class Panel extends Section {
constructor(name) {
super(name);
this._irf_settings = IRF__namespace.ui.panel.createTabFor(gm_info, {
tabName: name,
style: stylesheet
});
this.container = this._irf_settings.container;
}
render_header() {}
add_section(name, description) {
const section = new Section(name, description);
this.container.appendChild(section.container);
return section;
}
}
const panel = new Panel("Minimap");
// export const marker_panel = new Panel("Map markers");
web.delegateEvents(["input", "click"]);
// Settings page for the side menu
const section$7 = panel.add_section("Side menu", `You can access all map actions by right-clicking the map,
the car, or added markers. Use the toggles below to pin your favourite buttons to the map's side menu.`);
// Default contexts for the context menu
const contexts = ["Side", "Map", "Car", "Marker"];
// A Maplibre Control class that implements our context and side menus
const mapContainerEl$3 = await( IRF__namespace.dom.map);
class TricksControl {
// Control container
// Menu container
// Menu options container
// Menu label
// Settings container
constructor() {
this._context = undefined;
this.lat = 0;
this.lng = 0;
this.data = undefined;
this._c_cont = document.createElement('div'); // Control container
this._c_cont.className = 'maplibregl-ctrl maplibregl-ctrl-group mmt-side-control';
this._m_cont = document.createElement('div'); // Menu container
this._m_cont.id = "mmt-menu";
this._m_cont.style.display = "none";
mapContainerEl$3.querySelector('#mini-map').appendChild(this._m_cont);
document.addEventListener("click", () => {
this._hide_menu();
});
this._m_options = document.createElement('div'); // Menu options container
this._m_options.className = 'maplibregl-ctrl maplibregl-ctrl-group';
this._m_cont.appendChild(this._m_options);
const label_box = document.createElement('div');
label_box.id = "mmt-menu-label";
label_box.className = 'maplibregl-ctrl maplibregl-ctrl-group';
this._m_cont.appendChild(label_box);
this._m_label = document.createElement('span'); // Menu label
this._m_label.innerText = "Map";
const label = document.createElement('span');
label.innerText = " menu";
label.prepend(this._m_label);
label_box.appendChild(label);
const close = document.createElement('span');
close.innerText = "X";
close.id = "mmt-menu-close";
label_box.appendChild(close);
this._s_cont = document.createElement('div'); // Settings container
}
_show_menu() {
control._m_cont.style.display = "block";
mapContainerEl$3.classList.add("mmt-map-menu-opened");
}
_hide_menu() {
this._m_cont.style.display = "none";
mapContainerEl$3.classList.remove("mmt-map-menu-opened");
}
openMenu(context, lat, lng, left, top, data = undefined) {
this.context = context;
this.lat = lat;
this.lng = lng;
this.data = data;
this._m_cont.style.top = `${top}px`;
this._m_cont.style.left = `${left}px`;
this._show_menu();
}
set context(value) {
this._m_label.innerText = value;
this._m_cont.className = `mmt-menu-${value.replaceAll(' ', '-')}`;
this._context = value;
}
get context() {
return this._context;
}
onAdd(map) {
this._map = map;
return this._c_cont;
}
onRemove() {
this._c_cont.parentNode.removeChild(this._c_cont);
this._map = undefined;
}
addButton(icon, name, callback, context = undefined, {
side_visible_default = true,
before = undefined
} = {}) {
// Add side button
const returnValue = {
icon,
name,
callback,
contexts,
side_button: undefined,
side_icon: undefined,
side_checkbox: undefined,
context_button: undefined,
context_icon: undefined,
context_label: undefined
};
if (context == undefined || context.includes("Side")) {
const button = document.createElement("button");
settings[`side_${name}`] = GM_getValue(`side_${name}`, side_visible_default);
button.style.display = settings[`side_${name}`] ? "block" : "none";
button.title = name;
const checkbox = section$7.add_checkbox(`Show ${name}`, `side_${name}`, value => {
button.style.display = value ? "block" : "none";
});
const button_icon = document.createElement("span");
button_icon.className = "maplibregl-ctrl-icon";
button_icon.style.backgroundImage = `url("${icon}")`;
button_icon.style.backgroundSize = "contain";
button.appendChild(button_icon);
button.onclick = async () => {
this.context = "Side";
const vcontainer = await IRF__namespace.vdom.container;
this.lat = vcontainer.data.currentCoords.lat;
this.lng = vcontainer.data.currentCoords.lng;
this.data = undefined;
callback(this);
};
if (before) {
const sibling = Array.from(this._c_cont.children).filter(el => {
return el.title === before;
})[0];
sibling.insertAdjacentElement("beforebegin", button);
} else this._c_cont.appendChild(button);
returnValue.side_button = button;
returnValue.side_icon = button_icon;
returnValue.side_checkbox = checkbox;
}
const button = document.createElement("button");
if (context !== undefined) {
contexts.forEach(v => {
if (!context.includes(v)) button.classList.add(`mmt-hide-${v.replaceAll(' ', '-')}`);
});
}
const button_icon = document.createElement("span");
button_icon.className = "maplibregl-ctrl-icon";
button_icon.style.backgroundImage = `url("${icon}")`;
button_icon.style.backgroundSize = "contain";
button.appendChild(button_icon);
const button_label = document.createElement("span");
button_label.innerText = name;
button.appendChild(button_label);
button.onclick = () => {
callback(this);
};
if (before) {
const sibling = Array.from(this._m_options.children).filter(el => {
return el.innerText === before;
})[0];
sibling.insertAdjacentElement("beforebegin", button);
} else this._m_options.appendChild(button);
returnValue.context_button = button;
returnValue.context_icon = button_icon;
returnValue.context_label = button_label;
return returnValue;
}
}
// Define map controls to add buttons for
const control = new TricksControl();
function addContext(name, available) {
contexts.push(name);
const css_name = name.replaceAll(' ', '-');
Array.from(control._m_options.children).forEach(child => {
const child_children = child.children;
if (!available.includes(child_children[1].innerText)) {
child.classList.add(`mmt-hide-${css_name}`);
}
});
GM.addStyle(`
#mini-map {
.mmt-menu-${css_name} .mmt-hide-${css_name} {display: none !important;}
}`);
}
// Add the Control to the map and set up triggers for contex menus
const vmap$9 = await( IRF__namespace.vdom.map);
const ml_map$9 = vmap$9.data.map;
ml_map$9.addControl(control, "bottom-left");
ml_map$9.on("contextmenu", e => {
control.openMenu("Map", e.lngLat.lat, e.lngLat.lng, e.originalEvent.clientX, e.originalEvent.clientY);
});
const vcontainer$6 = await( IRF__namespace.vdom.container);
vmap$9.data.marker.getElement().oncontextmenu = e => {
e.stopPropagation();
e.preventDefault();
control.openMenu("Car", vcontainer$6.data.currentCoords.lat, vcontainer$6.data.currentCoords.lng, e.clientX, e.clientY);
};
// Add a compass Control
const maplibre$2 = await( IRF__namespace.modules.maplibre);
const compass = new maplibre$2.NavigationControl({
visualizePitch: true,
visualizeRoll: true,
showCompass: true,
showZoom: false
});
ml_map$9.addControl(compass, "bottom-left");
compass._container.style.display = settings.side_compass ? "block" : "none";
section$7.add_checkbox("Show compass", "side_compass", show => {
compass._container.style.display = show ? "block" : "none";
});
// Copy coordinates
control.addButton("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%22-6%20-6%2036%2036%22%20stroke-width%3D%221.5%22%20stroke%3D%22currentColor%22%20class%3D%22size-6%22%3E%3Cpath%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20d%3D%22M15.75%2017.25v3.375c0%20.621-.504%201.125-1.125%201.125h-9.75a1.125%201.125%200%200%201-1.125-1.125V7.875c0-.621.504-1.125%201.125-1.125H6.75a9%209%200%200%201%201.5.124m7.5%2010.376h3.375c.621%200%201.125-.504%201.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9%209%200%200%200-1.5-.124H9.375c-.621%200-1.125.504-1.125%201.125v3.5m7.5%2010.375H9.375a1.125%201.125%200%200%201-1.125-1.125v-9.25m12%206.625v-1.875a3.375%203.375%200%200%200-3.375-3.375h-1.5a1.125%201.125%200%200%201-1.125-1.125v-1.5a3.375%203.375%200%200%200-3.375-3.375H9.75%22%2F%3E%3C%2Fsvg%3E", "Copy coordinates", async c => {
let coords;
if (settings.coordinates_fancy) {
coords = geoCoordinatesParser.convert(`${c.lat},${c.lng}`).toCoordinateFormat("DMS").replaceAll(" ", "").replace(",", ", ");
} else {
coords = `${c.lat}, ${c.lng}`;
}
navigator.clipboard.writeText(coords);
}, undefined, {
side_visible_default: false
});
// Open Street View
const vcontainer$5 = await( IRF__namespace.vdom.container);
control.addButton("https://storage.googleapis.com/support-kms-prod/SNP_E2308F5561BE1525D2C88838252137BC5634_4353424_en_v0", "Open Street View", async c => {
const data = vcontainer$5.data;
// URL pattern from https://roadtrip.pikarocks.dev/
const url = "https://www.google.com/maps/@?api=1&map_action=pano" + `&viewpoint=${c.lat},${c.lng}` + (c.context === "Car" || c.context === "Side" ? `&pano=${data.currentPano}&heading=${data.currentHeading}` : "") + "&fov=90";
window.open(url, "_blank");
}, undefined, {
side_visible_default: true
});
// Open SV coverage map
control.addButton("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xml%3Aspace%3D%22preserve%22%20width%3D%22122.9%22%20height%3D%22122.9%22%3E%3Cpath%20d%3D%22M24.7%2062.4c1.8%201.6%203.6%203.2%205.4%204.6%202.4-3.6%205-7%208-10a7.2%207.2%200%200%201-.7-5.2c-3-1.9-6-4-9.2-6.4a38%2038%200%200%200-3.7%2017.3zm5.2-20.3c3.2%202.4%206.3%204.6%209.2%206.5a7.2%207.2%200%200%201%209.7-.8%2058.2%2058.2%200%200%201%2014.8-7%208%208%200%200%201%20.6-4c-4.4-3.8-9.6-7-15.6-10A37%2037%200%200%200%2029.9%2042zm23.7-16.8a75%2075%200%200%201%2012.7%208.5%208%208%200%200%201%204.6-2L72%2026a37%2037%200%200%200-18.4-.7zm21.9%202-1%205c2.4%201%204.3%203%205%205.5%203.3-.3%206.7-.3%2010.2-.1a37.7%2037.7%200%200%200-14.2-10.5zm17%2014.2c-4.5-.4-8.8-.4-12.9%200a8%208%200%200%201-2.5%204.4%2049%2049%200%200%201%206%2013.3h.8c3.3%200%206%202%207%205l7.4.2.1-3c0-7.3-2.1-14.2-5.9-20zM97.8%2068l-6.8-.1c-.6%202.8-2.8%205-5.6%205.6.1%203.3%200%206.9-.4%2010.6%202-.2%204.2-.5%206.3-1%203.3-4.3%205.5-9.5%206.5-15.1zm-4.4%2018.4a40.5%2040.5%200%200%201-32%2015.6A40.5%2040.5%200%200%201%2021%2061.4%2040.5%2040.5%200%200%201%2061.4%2021%2040.5%2040.5%200%200%201%20102%2061.4a40%2040%200%200%201-8.6%2025zm-5.7%201-3.1.4-.5%202.8%203.5-3zm-7.8%206%201-5.4c-6.6.4-13%200-19.1-1.4a6.5%206.5%200%200%201-5.4%202.3L53%2097.5c2.7.6%205.5%201%208.3%201v-.1c6.8%200%2013-1.8%2018.5-5zm-30.3%203%203.4-8.7a6.5%206.5%200%200%201-2.7-4.5%2086%2086%200%200%201-19.2-10.9l-3%205.3a37.1%2037.1%200%200%200%2021.5%2018.9zM26.4%2073.3l1.8-3.1-3.2-2.6c.3%202%20.8%203.9%201.4%205.7zM51%2050.7a7.2%207.2%200%200%201%20.4%204.9c4%201.9%207.9%203.4%2011.8%204.6a261%20261%200%200%200%204.1-13.5c-1-.6-1.8-1.5-2.5-2.5A55.5%2055.5%200%200%200%2051%2050.7zm-1.5%208a7.2%207.2%200%200%201-9%201c-2.7%202.8-5.2%206-7.5%209.5a83.2%2083.2%200%200%200%2018%2010.3%206.5%206.5%200%200%201%206.5-3.6c1.6-4%203-8%204.5-12.3a85.6%2085.6%200%200%201-12.5-4.9zm24.4-11a8.1%208.1%200%200%201-3.1.2l-4%2013.3c3.4.8%207%201.5%2010.6%202%20.6-1.1%201.4-2%202.4-2.7a46.8%2046.8%200%200%200-5.9-12.8zm7.8%2025.6a7.2%207.2%200%200%201-5-6.6c-3.8-.5-7.5-1.2-11.1-2a419%20419%200%200%201-4.7%2012.6%206.5%206.5%200%200%201%202.4%206%2070.3%2070.3%200%200%200%2018%201c.4-3.8.6-7.5.4-11z%22%20style%3D%22fill%3A%235fbdff%3Bfill-opacity%3A1%3Bstroke-width%3A.660746%22%2F%3E%3C%2Fsvg%3E", "Open SV coverage map", async c => {
const vmap = await IRF__namespace.vdom.map;
const url = "https://sv-map.netlify.app/#base=roadmap&cov=all&" + `panos=&zoom=${vmap.data.map.getZoom() + 1}¢er=${c.lat}%2C${c.lng}`;
window.open(url, "_blank");
}, undefined, {
side_visible_default: false
});
const vcontainer$4 = await( IRF__namespace.vdom.container);
const mapContainerEl$2 = await( IRF__namespace.dom.map);
const vmap$8 = await( IRF__namespace.vdom.map);
const ml_map$8 = vmap$8.data.map;
const event = new CustomEvent("toggleFullscreenMap");
// Implement toggling fullscreen mode
let getPanoUrlOverriden = false;
let mapIsFullscreen = false;
let changeStopArgs = undefined;
function toggleMapFullscreen(fullscreen) {
mapIsFullscreen = mapContainerEl$2.classList.toggle("fullscreen", fullscreen);
mapContainerEl$2.dispatchEvent(event);
if (mapIsFullscreen) changeStopArgs = undefined;
if (!mapIsFullscreen && changeStopArgs && vcontainer$4.data.endTime - Date.now() > 2000) vcontainer$4.methods.changeStop.apply(null, changeStopArgs);
if (!getPanoUrlOverriden) {
vcontainer$4.state.getPanoUrl = new Proxy(vcontainer$4.methods.getPanoUrl, {
apply: (target, thisArg, args) => {
if (mapIsFullscreen) return "about:blank";
return Reflect.apply(target, thisArg, args);
}
});
getPanoUrlOverriden = true;
}
}
// Save changeStop arguments for use when exiting fullscreen
const changeStop$1 = vcontainer$4.methods.changeStop;
vcontainer$4.state.changeStop = new Proxy(changeStop$1, {
apply: (target, thisArg, args) => {
changeStopArgs = args;
return Reflect.apply(target, thisArg, args);
}
});
// Add a button to the context menu
const fullscreen_icon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-6 -6 36 36" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"/></svg>`;
control.addButton(`data:image/svg+xml,${encodeURIComponent(fullscreen_icon)}`, "Fullscreen map", () => toggleMapFullscreen(), ["Side", "Map"], {
side_visible_default: true
});
// Go into fullscreen if #map is the window hash
ml_map$8.once("load", () => {
if (window.location.hash == "#map") toggleMapFullscreen(true);
});
const trash_svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.5" class="size-6" viewBox="-6 -6 36 36"><path stroke-linecap="round" stroke-linejoin="round" d="m14.7 9-.3 9m-4.8 0-.3-9m10-3.2 1 .2m-1-.2-1.1 13.9a2.3 2.3 0 0 1-2.3 2H8.1a2.3 2.3 0 0 1-2.3-2l-1-14m14.4 0a48.1 48.1 0 0 0-3.4-.3M3.8 6l1-.2m0 0a48.1 48.1 0 0 1 3.5-.4m7.5 0v-1c0-1.1-1-2-2.1-2.1a52 52 0 0 0-3.4 0c-1.1 0-2 1-2 2.2v.9m7.5 0a48.7 48.7 0 0 0-7.5 0"/></svg>';
const vcontainer$3 = await( IRF__namespace.vdom.container);
const vmap$7 = await( IRF__namespace.vdom.map);
const vodometer$1 = await( IRF__namespace.vdom.odometer);
const ml_map$7 = vmap$7.data.map;
// Code for measuring distance is heavily rewritten adapted into OOP from
// https://maplibre.org/maplibre-gl-js/docs/examples/measure/
// This object handles the abstract measuring functions
class Measure {
constructor() {
// GeoJSON object to hold our measurement features - points and a line
this.geojson_points = {
'type': 'FeatureCollection',
'features': []
};
this.geojson_line = {
'type': 'FeatureCollection',
'features': []
};
// Feature to draw the line between points
this.linestring = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: []
},
properties: {}
};
this.car = undefined;
}
toggleCar() {
if (this.car) {
this.removePoint(this.car);
this.car = undefined;
} else {
this.addPoint(vcontainer$3.data.currentCoords.lat, vcontainer$3.data.currentCoords.lng);
this.car = this.geojson_points.features[this.geojson_points.features.length - 1].properties.id;
}
}
updateCar() {
if (!this.car) return;
const coords = [vcontainer$3.data.currentCoords.lng, vcontainer$3.data.currentCoords.lat];
this.geojson_points.features.forEach(point => {
if (point.properties.id == this.car) point.geometry.coordinates = coords;else if (turf.distance(point.geometry.coordinates, coords) < 0.05) {
this.removePoint(point.properties.id);
}
});
this._updatePoints();
}
// Compute and display the distance determined by the line
setDistance() {
const unit = vodometer$1.data.isKilometers ? "km" : "mi";
const conversion = vodometer$1.data.isKilometers ? 1 : vodometer$1.data.conversionFactor;
let distance = turf.length(this.linestring);
// Assuming 10km/h
const time_est = distance / 10;
distance = distance / conversion;
distance_control.dist_cont.innerText = `${distance.toFixed(3)} ${unit}`;
distance_control.dist_cont.title = `~ ${Math.floor(time_est)}h ${Math.round(time_est % 1 * 60)}min (10km/h)`;
}
// Update the line based on the points
_recomputeLine() {
this.linestring.geometry.coordinates = this.geojson_points.features.map(point => {
return point.geometry.coordinates;
});
this.geojson_line.features = [this.linestring];
ml_map$7.getSource('geojson_line').setData(this.geojson_line);
}
// Update the points data on the map (and the distance based on that)
_updatePoints() {
this._recomputeLine();
ml_map$7.getSource('geojson_points').setData(this.geojson_points);
this.setDistance();
}
// Remove all points
clearPoints() {
this.geojson_points.features = [];
this._updatePoints();
this.car = undefined;
}
// Add a point at lat, lng
addPoint(lat, lng) {
const point = {
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [lng, lat]
},
'properties': {
'id': String(new Date().getTime())
}
};
this.geojson_points.features.push(point);
this._updatePoints();
}
// Remove a point with a given feature id
removePoint(id) {
this.geojson_points.features = this.geojson_points.features.filter(point => {
return point.properties.id !== id;
});
this._updatePoints();
}
async flyTo() {
ml_map$7.fitBounds(await ml_map$7.getSource('geojson_points').getBounds(), {
padding: 50
});
}
}
const measure = new Measure();
class DistanceControl {
// Control container
constructor() {
this._c_cont = document.createElement('div'); // Control container
this._c_cont.style.display = "none";
this._c_cont.className = 'maplibregl-ctrl maplibregl-ctrl-group mmt-distance-control';
const check_cont = document.createElement("div");
this._c_cont.appendChild(check_cont);
const check = document.createElement("input");
check.title = "Enable line editing";
check.type = "checkbox";
check_cont.appendChild(check);
this.check = check;
const dist_cont = document.createElement("div");
dist_cont.style.cursor = "pointer";
dist_cont.onclick = () => {
measure.flyTo();
};
dist_cont.innerText = "0 km";
this._c_cont.appendChild(dist_cont);
this.dist_cont = dist_cont;
const trash_button = document.createElement("button");
trash_button.title = "Discard and finish measuring";
trash_button.onclick = () => {
this.endMeasure();
};
this._c_cont.appendChild(trash_button);
this.trash_button = trash_button;
const button_icon = document.createElement("span");
button_icon.className = "maplibregl-ctrl-icon";
button_icon.style.backgroundImage = `url(data:image/svg+xml,${encodeURIComponent(trash_svg)})`;
button_icon.style.backgroundSize = "contain";
trash_button.appendChild(button_icon);
}
onAdd(map) {
this._map = map;
return this._c_cont;
}
onRemove() {
this._c_cont.parentNode.removeChild(this._c_cont);
this._map = undefined;
}
startMeasure() {
measure.clearPoints();
this.check.checked = true;
this._c_cont.style.display = "flex";
}
endMeasure() {
measure.clearPoints();
this.check.checked = false;
this._c_cont.style.display = "none";
}
}
const distance_control = new DistanceControl();
ml_map$7.addControl(distance_control, "top-left");
ml_map$7.once("load", () => {
// Add the two data sources
ml_map$7.addSource('geojson_line', {
'type': 'geojson',
'data': measure.geojson_line
});
ml_map$7.addSource('geojson_points', {
'type': 'geojson',
'data': measure.geojson_points
});
// Add layers and styles to the map
ml_map$7.addLayer({
id: 'measure-lines',
type: 'line',
source: 'geojson_line',
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': '#0009',
'line-width': 2.5
}
});
ml_map$7.addLayer({
id: 'measure-points',
type: 'circle',
source: 'geojson_points',
paint: {
'circle-radius': 5,
'circle-color': '#000b'
}
});
ml_map$7.moveLayer("measure-lines");
ml_map$7.moveLayer("measure-points");
// Handle clicking
ml_map$7.on('click', e => {
// Only interact with the measurements if the checkbox is ticked
if (!distance_control.check.checked) return;
// Did the user click any features?
const features = ml_map$7.queryRenderedFeatures(e.point, {
layers: ['measure-points']
});
if (features.length) {
// Remove the clicked point
measure.removePoint(features[0].properties.id);
} else {
// Add a new point
measure.addPoint(e.lngLat.lat, e.lngLat.lng);
}
});
vmap$7.data.marker.getElement().addEventListener("click", e => {
if (!distance_control.check.checked) return;
measure.toggleCar();
e.stopPropagation();
});
// Update the cursor as it moves over our new features
ml_map$7.on('mousemove', e => {
if (!distance_control.check.checked) {
// If we're not editing the measurements, stick to the default grab
ml_map$7.getCanvas().style.cursor = "grab";
} else {
const features = ml_map$7.queryRenderedFeatures(e.point, {
layers: ['measure-points']
});
// Pointer if hovering over a point, crosshair otherwise
ml_map$7.getCanvas().style.cursor = features.length ? 'pointer' : 'crosshair';
}
});
});
// Add context menu option
const ruler_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-200 -1160 1360 1360"><path d="M200-160v-340q0-142 99-241t241-99q142 0 241 99t99 241q0 142-99 241t-241 99H200Zm80-80h260q108 0 184-76t76-184q0-108-76-184t-184-76q-108 0-184 76t-76 184v260Zm260-120q58 0 99-41t41-99q0-58-41-99t-99-41q-58 0-99 41t-41 99q0 58 41 99t99 41Zm0-80q-25 0-42.5-17.5T480-500q0-25 17.5-42.5T540-560q25 0 42.5 17.5T600-500q0 25-17.5 42.5T540-440ZM80-160v-200h80v200H80Zm460-340Z"/></svg>';
control.addButton(`data:image/svg+xml,${encodeURIComponent(ruler_icon)}`, "Measure distance", async c => {
distance_control.startMeasure();
if (c.context === "Car") measure.toggleCar();else if (c.context !== "Side") measure.addPoint(c.lat, c.lng);
}, undefined, {
side_visible_default: false,
before: "Open Street View"
});
const section$6 = panel.add_section("Map position", `The map will follow the car by default.
You can change how (and if) this happens here.`);
section$6.add_checkbox("Re-centre map on the car after a timeout", "timeout_centre");
section$6.add_checkbox("Disable re-centring when map is in fullscreen", "timeout_centre_fullscreen_disable");
section$6.add_checkbox("Align map orientation with the car", "align_orientation");
section$6.add_checkbox("Reset zoom when the map re-centres", "reset_zoom");
section$6.add_slider("Default map zoom", "default_zoom");
const vcontainer$2 = await( IRF__namespace.vdom.container);
const vmap$6 = await( IRF__namespace.vdom.map);
const ml_map$6 = vmap$6.data.map;
// First flight will always want to be to the default zoom level
// So we need to figure out if said first flight has been achieved
let first_fly = true;
const zoom_subscription = ml_map$6.on("moveend", () => {
if (Math.abs(ml_map$6.getZoom() - settings.default_zoom) < 0.2) {
first_fly = false;
zoom_subscription.unsubscribe();
}
});
// General function for flying the map to a location
let latestBearing = 0;
function flyTo(coords, bearing, interactionOverride = true) {
const args = {
essential: true
};
if (coords) {
args.center = [coords[1], coords[0]];
}
if (bearing) {
args.bearing = bearing;
latestBearing = bearing;
} else if (settings.align_orientation) {
args.bearing = latestBearing;
}
if (first_fly || settings.reset_zoom) {
args.zoom = settings.default_zoom;
}
ml_map$6.flyTo(args, {
interactionOverride: interactionOverride
});
}
// Disable the default map reset function
// so we can implement our own logic for when this should happen
vmap$6.state.flyTo = new Proxy(vmap$6.methods.flyTo, {
apply: () => {}
});
// Proxy the user interaction handling to not include flyTo calls
// that have the interactionOverride flag
ml_map$6.off("dragstart", vmap$6.methods.handleUserInteraction);
ml_map$6.off("zoomstart", vmap$6.methods.handleUserInteraction);
vmap$6.state.handleUserInteraction = new Proxy(vmap$6.methods.handleUserInteraction, {
apply: (target, thisArg, args) => {
var _args$;
if (!((_args$ = args[0]) != null && _args$.interactionOverride)) {
return Reflect.apply(target, thisArg, args);
}
}
});
// Rebind event handlers so that they use the proxied method
ml_map$6.on("dragstart", vmap$6.methods.handleUserInteraction);
ml_map$6.on("zoomstart", vmap$6.methods.handleUserInteraction);
ml_map$6.on("rotatestart", vmap$6.methods.handleUserInteraction);
function checkUpdateMap() {
return Date.now() - vmap$6.data.lastUserInteraction > 30000 && (settings.timeout_centre && (!mapIsFullscreen || !settings.timeout_centre_fullscreen_disable) || vmap$6.data.lastUserInteraction == 0);
}
let prevPos = [0, 0];
vmap$6.data.marker.setLngLat = new Proxy(vmap$6.data.marker.setLngLat, {
apply: (target, thisArg, args) => {
// Sync map to the coordinates when the marker is updated
// but only if the marker moved a significant distance compared to the tile size
const map_lnglat = ml_map$6.getCenter();
const diff = [Math.abs(args[0][0] - map_lnglat.lng), Math.abs(args[0][1] - map_lnglat.lat)];
const tile_width = 360 / 2 ** ml_map$6.getZoom();
const factor = 0.01;
if ((diff[0] > tile_width * factor || diff[1] > tile_width * factor) && checkUpdateMap()) {
flyTo([args[0][1], args[0][0]]);
}
// Update the distance measurement when the car's position changes
// (this method is called on every websocket receive, so we filter it here)
if (args[0][0] !== prevPos[0] || args[0][1] !== prevPos[1]) {
measure.updateCar();
prevPos = args[0];
}
return Reflect.apply(target, thisArg, args);
}
});
// Add button for re-centring
control.addButton("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E", "Centre", async c => {
flyTo([c.lat, c.lng], settings.align_orientation && (c.context === "Side" || c.context === "Car") ? vcontainer$2.data.currentHeading : undefined);
if (c.context === "Side" || c.context === "Car") {
vmap$6.state.lastUserInteraction = 0;
}
}, ["Side", "Car", "Marker"]);
const maplibre$1 = await( IRF__namespace.modules.maplibre);
const vmap$5 = await( IRF__namespace.vdom.map);
const ml_map$5 = vmap$5.data.map;
// In memory marker storage
const markers = {};
class MMTMarker extends maplibre$1.Marker {
_mmt_remove() {
delete settings.markers[this._mmt_id];
delete markers[this._mmt_id];
GM.setValues(settings);
this.remove();
}
}
async function add_marker(lat, lng, marker_id, color) {
color = color ? color : settings.marker_color;
const marker = new MMTMarker({
draggable: settings.draggable_markers,
scale: 0.8,
color: color
}).setLngLat([lng, lat]).addTo(ml_map$5);
if (!marker_id) {
marker_id = crypto.randomUUID();
settings.markers[marker_id] = [lat, lng, {
color
}];
GM.setValues(settings);
}
marker._mmt_id = marker_id;
markers[marker_id] = marker;
marker.on("dragend", () => {
const lngLat = marker.getLngLat();
settings.markers[marker_id][0] = lngLat.lat;
settings.markers[marker_id][1] = lngLat.lng;
GM.setValues(settings);
});
marker.getElement().addEventListener("contextmenu", f => {
f.stopPropagation();
f.preventDefault();
const colour = marker.getElement().children[0].children[0].children[1].getAttribute("fill");
mcol_input.value = colour;
const lngLat = marker.getLngLat();
control.openMenu("Marker", lngLat.lat, lngLat.lng, f.clientX, f.clientY, marker);
});
}
// Add the markers from extension storage
for (const [marker_id, value] of Object.entries(settings.markers)) {
add_marker(value[0], value[1], marker_id, value[2].color);
}
// Basic context menu options
const marker_icon_base = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%22-5%20-6%2037%2036%22%20stroke-width%3D%221.5%22%20stroke%3D%22currentColor%22%20class%3D%22size-6%22%3E%3Cpath%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20d%3D%22M15%2010.5a3%203%200%201%201-6%200%203%203%200%200%201%206%200%22%2F%3E%3Cpath%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20d%3D%22M19.5%2010.5c0%207.142-7.5%2011.25-7.5%2011.25S4.5%2017.642%204.5%2010.5a7.5%207.5%200%201%201%2015%200%22%2F%3E";
control.addButton(marker_icon_base + "%3Cpath%20d%3D%22M19%2021h8m-4-4v8%22%2F%3E%3C%2Fsvg%3E", "Add marker", async c => {
add_marker(c.lat, c.lng);
}, ["Side", "Car", "Map"], {
side_visible_default: false,
before: "Centre"
});
control.addButton(marker_icon_base + "%3Cpath%20d%3D%22M20%2018l6%206m-6%200l6%20-6%22%2F%3E%3C%2Fsvg%3E", "Remove marker", async c => {
c.data._mmt_remove();
}, ["Marker"]);
// Mark given coordinates
control.addButton("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%22-6%20-6%2036%2036%22%20stroke-width%3D%221.5%22%20stroke%3D%22currentColor%22%20class%3D%22size-6%22%3E%3Cpath%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20d%3D%22M6%2012%203.269%203.125A59.8%2059.8%200%200%201%2021.485%2012%2059.8%2059.8%200%200%201%203.27%2020.875L5.999%2012Zm0%200h7.5%22%2F%3E%3C%2Fsvg%3E", "Go to and mark coordinates", async () => {
let converted;
try {
converted = geoCoordinatesParser.convert(prompt("Input coordinates here:"));
} catch (_unused) {
alert("Coordinates were incorrect!");
return;
}
add_marker(converted.decimalLatitude, converted.decimalLongitude);
flyTo([converted.decimalLatitude, converted.decimalLongitude], undefined, false);
}, ["Side", "Map"], {
side_visible_default: false,
before: "Copy coordinates"
});
// Draggable markers
const draggable_meta = control.addButton("", "Draggable markers", () => {
settings.draggable_markers = !settings.draggable_markers;
GM.setValues(settings);
draggable_checkbox.checked = settings.draggable_markers;
for (const [, marker] of Object.entries(markers)) {
marker.setDraggable(settings.draggable_markers);
}
}, ["Marker"], {
before: "Remove marker"
});
const draggable_checkbox = document.createElement("input");
draggable_checkbox.type = "checkbox";
draggable_checkbox.checked = settings.draggable_markers;
draggable_meta.context_icon.appendChild(draggable_checkbox);
draggable_meta.context_icon.classList.add("mmt-draggable-checkbox-icon");
// Marker colour
const dropper_svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-6 -6 36 36" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="m15 11.25 1.5 1.5.75-.75V8.758l2.276-.61a3 3 0 1 0-3.675-3.675l-.61 2.277H12l-.75.75 1.5 1.5M15 11.25l-8.47 8.47c-.34.34-.8.53-1.28.53s-.94.19-1.28.53l-.97.97-.75-.75.97-.97c.34-.34.53-.8.53-1.28s.19-.94.53-1.28L12.75 9M15 11.25 12.75 9"/></svg>';
control.addButton(`data:image/svg+xml,${encodeURIComponent(dropper_svg)}`, "Set color", () => {
mcol_input.click();
}, ["Marker"], {
before: "Remove marker"
});
const mcol_input = document.createElement("input");
mcol_input.type = "color";
mcol_input.id = "mmt-menu-color";
mcol_input.addEventListener("input", () => {
if (control.data) {
control.data.getElement().children[0].children[0].children[1].setAttribute("fill", mcol_input.value);
settings.markers[control.data._mmt_id][2].color = mcol_input.value;
GM.setValues(settings);
}
});
// Marker settings
const section$5 = panel.add_section("User markers", `You can add and remove
your own markers by right-clicking the minimap.`);
section$5.add_input("Default marker colour", "marker_color", "color", undefined, "#f7a000");
section$5.add_button("Remove all markers", () => {
for (const [, marker] of Object.entries(markers)) {
marker._mmt_remove();
}
});
const section$4 = panel.add_section("Measurements and units", `Do you prefer metric? (correct) Or would you like more precision?`);
const vcontainer$1 = await( IRF__namespace.vdom.container);
const vmap$4 = await( IRF__namespace.vdom.map);
const ml_map$4 = await( vmap$4.data.map);
const maplibre = await( IRF__namespace.modules.maplibre);
const vodometer = await( IRF__namespace.vdom.odometer);
const odometer_el = await( IRF__namespace.dom.odometer);
// Add a scale bar
const scale_control = new maplibre.ScaleControl({
unit: vodometer.data.isKilometers ? "metric" : "imperial"
});
ml_map$4.addControl(scale_control, "bottom-right");
scale_control._container.style.margin = "0px 36px 5px 0px";
scale_control._container.style.display = settings.show_scale ? "block" : "none";
// Sync the scale bar units to the odometer
// Get the original setter
const {
set: isKilometersSetter
} = Object.getOwnPropertyDescriptor(vodometer.state, 'isKilometers');
// Override the setter
Object.defineProperty(vodometer.state, 'isKilometers', {
set(isKilometers) {
const r_value = isKilometersSetter.call(this, isKilometers);
// Set the units on the scale bar
scale_control.setUnit(isKilometers ? "metric" : "imperial");
// Update the units on the distance measurement
measure.setDistance();
return r_value;
},
configurable: true,
enumerable: true
});
// Add a settings checkbox for showing the scale bar
section$4.add_checkbox("Show map scale", "show_scale", show => {
scale_control._container.style.display = show ? "block" : "none";
});
// Default to kilometres if desired
if (settings.km_units) {
vodometer.state.isKilometers = true;
}
section$4.add_checkbox("Default to metric units", "km_units", async value => {
vodometer.state.isKilometers = value;
});
// Display decimal points if desired
const decimal_el = document.createElement("span");
const units_el = odometer_el.getElementsByClassName("miles-text")[0];
decimal_el.style.display = "none";
units_el.appendChild(decimal_el);
vcontainer$1.state.updateData = new Proxy(vcontainer$1.methods.updateData, {
apply: (target, thisArg, args) => {
// debugger;
let distance = args[0]["distance"];
if (vodometer.data.isKilometers) {
distance *= vodometer.data.conversionFactor;
}
const decimals = (distance % 1).toFixed(2);
decimal_el.innerHTML = `<br>${decimals.substring(1)}`;
return Reflect.apply(target, thisArg, args);
}
});
section$4.add_checkbox("Show decimals in distance", "decimal_units", async value => {
if (value) {
odometer_el.classList.toggle("mmt-miles-decimal", true);
} else {
odometer_el.classList.toggle("mmt-miles-decimal", false);
}
});
odometer_el.classList.toggle("mmt-miles-decimal", settings.decimal_units);
// This setting gets used by "copy coordinates"
section$4.add_checkbox("Use minutes and seconds when copying coordinates", "coordinates_fancy");
const vcontainer = await( IRF__namespace.vdom.container);
const vmap$3 = await( IRF__namespace.vdom.map);
const ml_map$3 = vmap$3.data.map;
const car_marker = vmap$3.data.marker;
const marker_el = vmap$3.data.marker.getElement();
// Correct car marker offset (it's a little off-centre by default)
function default_marker_svg() {
return 'url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20101%20245%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xml%3Aspace%3D%22preserve%22%20style%3D%22fill-rule%3Aevenodd%3Bclip-rule%3Aevenodd%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%3Bstroke-miterlimit%3A1.5%22%3E%3Cg%20transform%3D%22translate(-118.117%20-1517)%22%3E%3Cpath%20d%3D%22M219%201598h-88.922l22.231-94h44.461z%22%20style%3D%22fill%3Aurl(%23a)%22%20transform%3D%22matrix(-1.13495%200%200%20-1.05851%20366.671%203208.5)%22%2F%3E' + (settings.car_marker_custom ? '' : '%3Ccircle%20cx%3D%22168.578%22%20cy%3D%221636.5%22%20r%3D%2238.5%22%20style%3D%22fill%3A%2300a6ff%3Bstroke%3A%23fff%3Bstroke-width%3A8.33px%22%2F%3E') + '%3C%2Fg%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%220%22%20gradientUnits%3D%22userSpaceOnUse%22%20gradientTransform%3D%22matrix(0%2094%20-114%200%20174.539%201504)%22%3E%3Cstop%20offset%3D%220%22%20style%3D%22stop-color%3A%2300a6ff%3Bstop-opacity%3A.77%22%2F%3E%3Cstop%20offset%3D%221%22%20style%3D%22stop-color%3A%2300a6ff%3Bstop-opacity%3A0%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E")';
}
marker_el.style.backgroundImage = default_marker_svg();
marker_el.style.width = `${settings.car_marker_size}px`;
marker_el.style.height = `${settings.car_marker_size}px`;
marker_el.style.setProperty('--marker-opacity', settings.marker_opacity.toString());
// Custom car marker
const custom_car = document.createElement("img");
custom_car.src = settings.car_marker_url;
custom_car.style.maxWidth = `${settings.car_marker_scale}%`;
custom_car.style.maxHeight = `${settings.car_marker_scale}%`;
custom_car.style.rotate = `${settings.car_marker_rotation}deg`;
custom_car.style.display = settings.car_marker_custom ? "block" : "none";
marker_el.appendChild(custom_car);
// Set the marker rotation when the car moves
const changeStop = vcontainer.methods.changeStop;
vcontainer.state.changeStop = new Proxy(changeStop, {
apply: (target, thisArg, args) => {
const returnValue = Reflect.apply(target, thisArg, args);
car_marker.setRotation(args[3]);
if (checkUpdateMap() && settings.align_orientation && Math.abs(ml_map$3.getBearing() - args[3]) % 360 > 1) flyTo(undefined, args[3]);
const x_flip = settings.car_marker_flip ? "-1" : "1";
if (settings.car_marker_flip_x && args[3] > 180) {
custom_car.style.transform = `scale(${x_flip}, -1)`;
} else {
custom_car.style.transform = `scale(${x_flip}, 1)`;
}
return returnValue;
}
});
// Override the normal marker rotation setting method, we do it above!
vmap$3.state.setMarkerRotation = new Proxy(vmap$3.methods.setMarkerRotation, {
apply: () => {}
});
// Settings
const section$3 = panel.add_section("Car marker", `You can set the car marker on the map to be
any custom image, or change the appearance of the default marker too.`);
section$3.add_slider("Car marker opacity", "marker_opacity", value => {
marker_el.style.setProperty('--marker-opacity', value);
}, [0, 1, 0.05]);
section$3.add_slider("Car marker size (px)", "car_marker_size", value => {
marker_el.style.width = `${value}px`;
marker_el.style.height = `${value}px`;
}, [20, 100, 1]);
section$3.add_checkbox("Custom car marker", "car_marker_custom", show => {
custom_car.style.display = show ? "block" : "none";
marker_el.style.backgroundImage = default_marker_svg();
});
section$3.add_input("Car marker image URL", "car_marker_url", "", () => custom_car.src = settings.car_marker_url, "https://files.catbox.moe/a55qk5.png");
section$3.add_comment(`Default white van picture:
<a href='https://www.vecteezy.com/free-png/2d-delivery-truck-top-view' target='_blank'>
2d Delivery Truck Top View PNGs by Vecteezy</a>.
You can upload your custom image to <a href='https://catbox.moe' target='_blank'>catbox.moe</a>
to get an image URL.`);
section$3.add_slider("Custom car marker scale (%)", "car_marker_scale", value => {
custom_car.style.maxWidth = `${value}%`;
custom_car.style.maxHeight = `${value}%`;
}, [0, 100, 1]);
section$3.add_slider("Custom car marker rotation (deg)", "car_marker_rotation", value => {
custom_car.style.rotate = `${value}deg`;
}, [0, 360, 5]);
section$3.add_checkbox("Flip custom image", "car_marker_flip");
section$3.add_checkbox("Flip image when going left", "car_marker_flip_x");
const vmap$2 = await( IRF__namespace.vdom.map);
const ml_map$2 = vmap$2.data.map;
const mapContainerEl$1 = await( IRF__namespace.dom.map);
const miniMapEl = mapContainerEl$1.querySelector('#mini-map');
const expandButtonEl = mapContainerEl$1.querySelector('.expand-button');
// Automatically expand the map
if (window.innerWidth > 900 && settings.expand_map) {
vmap$2.state.isExpanded = true;
}
// Set the variables for map resizing if not undefined
function setMiniMapSize({
width,
height,
expanded_width,
expanded_height
}) {
miniMapEl.style.setProperty('--map-width', width ? `${Math.min(Math.max(0, width), 90)}vw` : "");
miniMapEl.style.setProperty('--map-height', height ? `${Math.min(Math.max(0, height), 90)}vh` : "");
miniMapEl.style.setProperty('--map-width-expanded', expanded_width ? `${Math.min(Math.max(0, expanded_width), 90)}vw` : "");
miniMapEl.style.setProperty('--map-height-expanded', expanded_height ? `${Math.min(Math.max(0, expanded_height), 90)}vh` : "");
}
// Set initial map size and resize oncee the css properties are applied
setMiniMapSize(settings.map_size);
requestAnimationFrame(() => {
ml_map$2.resize();
});
// Drag to resize
let isClicked = false; // Clicked determines if we should be listening to mousemove
let isResizing = false; // Resizing determines if the expanded state should be switched
let lastX, lastY;
// Start the drag
expandButtonEl.addEventListener('mousedown', e => {
isClicked = true;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
});
// Continue the drag
document.addEventListener('mousemove', e => {
if (!isClicked) return;
if (e.buttons == 0) {
isClicked = false;
isResizing = false;
return;
}
const deltaX = e.clientX - lastX;
const deltaY = e.clientY - lastY;
// Set the resizing flag if we moved
// The call to switch expanded state will then not be sent
isResizing = true;
const currentSizePx = {
width: miniMapEl.offsetWidth,
height: miniMapEl.offsetHeight
};
const e_mod = mapContainerEl$1.classList.contains("expanded") ? "expanded_" : "";
settings.map_size[e_mod + "width"] = (currentSizePx.width + deltaX) / window.innerWidth * 100;
settings.map_size[e_mod + "height"] = (currentSizePx.height - deltaY) / window.innerHeight * 100;
setMiniMapSize(settings.map_size);
GM.setValues(settings);
lastX = e.clientX;
lastY = e.clientY;
});
// End drag
document.addEventListener('mouseup', () => {
isClicked = false;
});
// Overriding the isExpanded setter, as overriding toggleExpand doesn't seem to work
// the first time it's called. We want to prevent this variable from being flipped if
// the map is being resized (and the game will try to flip it when the mouse is released).
const {
set: isExpandedSetter
} = Object.getOwnPropertyDescriptor(vmap$2.state, 'isExpanded');
Object.defineProperty(vmap$2.state, 'isExpanded', {
set(isExpanded) {
if (isResizing) {
isResizing = false;
return isExpandedSetter.call(this, !isExpanded);
}
return isExpandedSetter.call(this, isExpanded);
},
configurable: true,
enumerable: true
});
// Settings
const section$2 = panel.add_section("Map size", `You can drag the "expand"
button of the map to change its size, and you can save two different sizes this way
- expanded and not expanded. Click the "expand" button to toggle between these.`);
section$2.add_button("Reset map size", () => {
settings.map_size = {
width: undefined,
height: undefined,
expanded_width: undefined,
expanded_height: undefined
};
GM.setValues(settings);
setMiniMapSize(settings.map_size);
});
section$2.add_checkbox("Expand the map by default", "expand_map");
const section$1 = panel.add_section("Map appearance", `Change the opacity of
map elements here. You can use the sliders below to make the map mostly transparent,
or even set it so that only the streets are visible unless you put your mouse over it!`);
const vmap$1 = await( IRF__namespace.vdom.map);
const ml_map$1 = vmap$1.data.map;
const mapContainerEl = await( IRF__namespace.dom.map);
// Map background layer opacity
function setLayerOpacity(value = undefined) {
if (!value) {
value = vmap$1.data.isExpanded ? settings.background_opacity_expanded : settings.background_opacity;
}
value = parseFloat(value);
ml_map$1.setPaintProperty("background", "background-opacity", value);
ml_map$1.setPaintProperty("water", "fill-opacity", value);
}
// Hide the menu and change background opacities when netux's PIP exits/enters
let inPIP = false;
if (window.documentPictureInPicture) {
window.documentPictureInPicture.addEventListener("enter", e => {
setLayerOpacity(1);
inPIP = true;
e.window.addEventListener("pagehide", () => {
control._hide_menu();
setLayerOpacity();
inPIP = false;
});
});
}
// Map element opacity
mapContainerEl.style.opacity = settings.map_opacity;
mapContainerEl.style.setProperty('--map-opacity-expanded', settings.map_opacity_expanded);
// Set the route opacity
ml_map$1.once('load', () => {
// Messing with styles should only happen once map is ready
ml_map$1.setPaintProperty("route", "line-opacity", parseFloat(settings.route_opacity));
setLayerOpacity();
// Full layer opacity when mouse over the map
mapContainerEl.addEventListener("mouseenter", () => {
if (inPIP || mapIsFullscreen) return;
setLayerOpacity(1);
});
mapContainerEl.addEventListener("mouseleave", () => {
if (inPIP || mapIsFullscreen) return;
setLayerOpacity();
});
});
// Full opacity when map is in fullscreen
mapContainerEl.addEventListener("toggleFullscreenMap", () => {
console.log("setting map opacity!");
setLayerOpacity(mapIsFullscreen ? 1 : undefined);
});
// Set the old route opacity once it's added
const old_route_subscription = ml_map$1.on("data", e => {
if (e.sourceId == "old-route") {
ml_map$1.setPaintProperty("old-route-layer", "line-opacity", parseFloat(settings.route_opacity));
ml_map$1.moveLayer("old-route-layer", "boundary_3");
old_route_subscription.unsubscribe();
}
});
// Settings
section$1.add_slider("Collapsed map opacity", "map_opacity", value => {
mapContainerEl.style.opacity = value;
}, [0, 1, 0.05]);
section$1.add_slider("Collapsed map background opacity", "background_opacity", value => {
if (!vmap$1.data.isExpanded) setLayerOpacity(value);
}, [0, 1, 0.05]);
section$1.add_slider("Expanded map opacity", "map_opacity_expanded", value => {
mapContainerEl.style.setProperty('--map-opacity-expanded', value);
}, [0, 1, 0.05]);
section$1.add_slider("Expanded map background opacity", "background_opacity_expanded", value => {
if (vmap$1.data.isExpanded) setLayerOpacity(value);
}, [0, 1, 0.05]);
section$1.add_slider("Route opacity", "route_opacity", value => {
ml_map$1.setPaintProperty("route", "line-opacity", parseFloat(value));
ml_map$1.setPaintProperty("old-route-layer", "line-opacity", parseFloat(value));
}, [0, 1, 0.05]);
const vmap = await( IRF__namespace.vdom.map);
const ml_map = vmap.data.map;
// Add layers once the map is ready
ml_map.once("load", () => {
ml_map.addSource('sv', {
type: 'raster',
tiles: ['https://mts.googleapis.com/vt?pb=%211m4%211m3%211i{z}%212i{x}%213i{y}%212m8%211e2%212ssvv%214m2%211scc%212s*211m3*211e2*212b1*213e2*212b1*214b1%214m2%211ssvl%212s*212b1%213m11%212sen%213sUS%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%215m1%215f2'],
tileSize: 256
});
ml_map.addSource('ugc_sv', {
type: 'raster',
tiles: ['https://mts.googleapis.com/vt?pb=%211m4%211m3%211i{z}%212i{x}%213i{y}%212m8%211e2%212ssvv%214m2%211scc%212s%2A211m3%2A211e3%2A212b1%2A213e2%2A211m3%2A211e10%2A212b1%2A213e2%2A212b1%2A214b1%214m2%211ssvl%212s%2A212b1%213m16%212sen%213sUS%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.e%7Cp.c%3A%23ff0000%2Cs.e%3Ag.f%7Cp.c%3A%23bd5f1b%2Cs.e%3Ag.s%7Cp.c%3A%23f7ca9e%2C%215m1%215f2%0A'],
tileSize: 256
});
ml_map.addLayer({
id: 'sv-tiles',
type: 'raster',
source: 'sv',
minzoom: 0,
maxzoom: 22,
layout: {
visibility: settings.coverage ? "visible" : "none"
},
paint: {
"raster-opacity": parseFloat(settings.coverage_opacity)
}
}, "route");
ml_map.addLayer({
id: 'svugc-tiles',
type: 'raster',
source: 'ugc_sv',
minzoom: 0,
maxzoom: 22,
layout: {
visibility: settings.coverage ? "visible" : "none"
},
paint: {
"raster-opacity": parseFloat(settings.coverage_opacity)
}
}, "route");
// Move the layers below some others that I would like on top
ml_map.moveLayer("route", "boundary_3");
ml_map.moveLayer("svugc-tiles", "route");
ml_map.moveLayer("sv-tiles", "svugc-tiles");
});
// Settings
const section = panel.add_section("SV coverage", `Include official and unofficial SV coverage
on the map. Official lines are shown in blue, unofficial lines are shown in orange.
You may see a brown-ish colour where the two overlap. Photospheres are shown as red circles.`);
section.add_checkbox("Show coverage", "coverage", value => {
["svugc-tiles", "sv-tiles"].forEach(kind => {
ml_map.setLayoutProperty(kind, "visibility", value ? "visible" : "none");
});
});
section.add_slider("Coverage opacity", "coverage_opacity", value => {
["svugc-tiles", "sv-tiles"].forEach(kind => {
ml_map.setPaintProperty(kind, "raster-opacity", parseFloat(value));
});
}, [0, 1, 0.05]);
GM.addStyle(css_248z);
// import './kml';
// Export some APIs
unsafeWindow._MMT_control = control;
unsafeWindow._MMT_addContext = addContext;
unsafeWindow._MMT_getMarkers = () => {
return markers;
};
})(VM.solid.web, IRF, VM.solid, geoCoordinatesParser, turf);