Internet Roadtrip Minimap tricks

Provide some bonus options for the Internet Roadtrip minimap.

当前为 2025-07-09 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Internet Roadtrip Minimap tricks
// @namespace   jdranczewski.github.io
// @match       https://neal.fun/internet-roadtrip/*
// @version     0.5.4
// @author      jdranczewski (+netux +GameRoMan)
// @description Provide some bonus options for the Internet Roadtrip minimap.
// @license     MIT
// @icon         https://files.catbox.moe/v4yu3f.png
// @grant        GM.setValues
// @grant        GM.getValues
// @grant        GM.addStyle
// @grant        unsafeWindow
// @require     https://cdn.jsdelivr.net/npm/[email protected]
// @require      https://cdn.jsdelivr.net/gh/ianengelbrecht/geo-coordinates-parser@b06d051f2a70bc95c2fa1a063ceef85f19823fee/bundle/geocoordsparser.js
// @require      https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
// ==/UserScript==

// This works together with irf.d.ts to give us type hints
/**
 * Internet Roadtrip Framework
 * @typedef {typeof import('internet-roadtrip-framework')} IRF
 */

(async function() {
    // Get map methods and various objects
    const map = await IRF.vdom.map;
    const odometer = await IRF.vdom.odometer;
    const odometer_el = await IRF.dom.odometer;
    const ml_map = map.data.map;
    const mapMethods = map.methods;
    const mapContainerEl = await IRF.dom.map;
    const miniMapEl = mapContainerEl.querySelector('#mini-map');
    const expandButtonEl = mapContainerEl.querySelector('.expand-button');
    const marker_el = map.data.marker.getElement();
    const vcontainer = await IRF.vdom.container;
    const maplibre = await IRF.modules.maplibre;
    let measure;

    // Custom styles
    GM.addStyle(`
    @media (min-width: 900px) {
        .map-container {
            & .expand-button {
                cursor: nesw-resize;
                display: flex !important;
            }

            & #mini-map {
                position: relative;
                width: var(--map-width, 250px) !important;
                height: var(--map-height, 170px) !important;
            }
        }
        .expanded #mini-map {
            width: var(--map-width-expanded, 450px) !important;
            height: var(--map-height-expanded, 300px) !important;
        }
        .expanded .expand-button img {
            rotate: 180deg;
        }
        .expanded {
            opacity: var(--map-opacity-expanded, 1) !important;
        }
    }
    .map-container {
        transition: opacity .5s;
        &:hover {
            opacity: 1 !important;
        }
    }
    .maplibregl-marker {
        opacity: var(--marker-opacity, 1) !important;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .mmt-map-menu-opened {
        opacity: 1 !important;
    }

    /* Putting this in #mini-map so Netux's PIP script
       can copy the styles correctly */
    #mini-map {
        #mmt-menu {
            position: fixed;
            z-index: 1000;
            transform: translate(0, -100%);
            & button {
                width: 100%;
                text-align: left;
                display: flex;
                align-items: center;
            }
            & .maplibregl-ctrl-icon {
                width: 29px;
            }
            & .maplibregl-ctrl-icon + span {
                margin: 0 9px 0 5px;
                display: flex;
                align-items: center;
            }
            & #mmt-menu-label {
                margin: 5px 0 0 0;
                font-size: 14px;
                padding: 6px;
                background: #f1f1f1;
                display: flex;
                justify-content: space-between;
                & #mmt-menu-close {
                    margin-left: 10px;
                    margin-right: 2px;
                    cursor: pointer;
                }
            }

            & #mmt-menu-color {
                display: none;
            }
        }
        .mmt-menu-Map .mmt-hide-Map {display: none !important;}
        .mmt-menu-Marker .mmt-hide-Marker {display: none !important;}
        .mmt-menu-Car .mmt-hide-Car {display: none !important;}

        /* For debugging */
        .mmt-menu-Map .mmt-hide-Map {opacity: 0.5 !important;}
        .mmt-menu-Marker .mmt-hide-Marker {opacity: 0.5 !important;}
        .mmt-menu-Car .mmt-hide-Car {opacity: 0.5 !important;}

        .mmt-distance-control {
            display: flex;
            & div {
                display: flex;
                align-items: center;
                padding: 0px 5px;
                border-right: 1px solid #ddd;
            }
        }
    }

    /* Decimal points */
    .mmt-miles-decimal .miles-text {
        text-align: center;
        line-height: 10px;
        & span {
            display: inline !important;
            font-size: 10px;
        }
    }

    /* For Netux's PIP script */
    @media (display-mode: picture-in-picture) {
        .maplibregl-ctrl-scale {
            margin: 0px 5px 5px 0px !important;
        }
        #mini-map {
            #mmt-menu {
                transform: translate(0, 0px) !important;
                top: 10px !important;
                right: 10px !important;
                left: auto !important;
            }
        }
    }
    `);

    // Settings
    const settings = {
        "expand_map": false,
        "default_zoom": 12.5,
        "timeout_centre": true,
        "reset_zoom": 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": {},

        "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,
        "side_Go to coordinates": false,
        "side_Copy coordinates": false,
        "side_Measure distance": false,
        "side_Open Street View": true,
        "side_Open SV coverage map": true,
        "side_Add marker": false,
        "side_Centre": true,

        "coverage": true,
        "coverage_opacity": 0.75,
    }
    const storedSettings = await GM.getValues(Object.keys(settings))
    Object.assign(
        settings,
        storedSettings
    );
    await GM.setValues(settings);

    // Settings panel GUI
    let gm_info = GM.info
    gm_info.script.name = "Minimap tricks"
    const irf_settings = IRF.ui.panel.createTabFor(
        gm_info, {
            tabName: "Minimap",
            style: "a {color: #aaa};"
        }
    );

    // Add UI button to reset map scale
    {
        let button = document.createElement("button");
        button.style.marginRight = "10px";
        button.innerText = "Reset map size";
        button.onclick = () => {
            settings.map_size = {};
            GM.setValues(settings);
            setMiniMapSize(settings.map_size);
        }
        irf_settings.container.appendChild(button);
    }

    // Add UI button to remove all markers
    {
        let button = document.createElement("button");
        button.innerText = "Remove all markers";
        button.className = "mmt-button";
        button.onclick = () => {
            for (const [marker_id, marker] of Object.entries(markers)) {
                marker._mmt_remove();
            }
        }
        irf_settings.container.appendChild(button);
    }
    irf_settings.container.appendChild(document.createElement("br"));
    irf_settings.container.appendChild(document.createElement("br"));

    // Checkboxes
    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.onchange = () => {
            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"));
    }
    add_checkbox("Auto-expand map", "expand_map");
    add_checkbox("Re-centre map after a timeout", "timeout_centre");
    add_checkbox("Reset zoom with map re-centre", "reset_zoom");

    function add_slider(
        name, identifier, callback=undefined,
        slider_bits=[1, 17, .5]
    ) {
        let label = document.createElement("label");

        let text = document.createElement("span");
        text.innerText = " " + name + ": ";
        label.appendChild(text);

        let value_label = document.createElement("span");
        value_label.innerText = settings[identifier];
        label.appendChild(value_label);

        let slider = document.createElement("input");
        slider.type = "range";
        slider.min = slider_bits[0];
        slider.max = slider_bits[1];
        slider.step = slider_bits[2];
        slider.value = settings[identifier];
        slider.className = IRF.ui.panel.styles.slider;
        label.appendChild(slider);

        slider.oninput = () => {
            settings[identifier] = slider.value;
            value_label.innerText = slider.value;
            GM.setValues(settings);
            if (callback) callback(slider.value);
        }
        slider.onmousedown = (e) => {e.stopPropagation()}

        irf_settings.container.appendChild(label);
        irf_settings.container.appendChild(document.createElement("br"));
        irf_settings.container.appendChild(document.createElement("br"));
    }
    add_slider("Default map zoom", "default_zoom");

    // Fly to a location
    let first_fly = true;
    const zoom_subscription = ml_map.on("moveend", () => {
        if (Math.abs(ml_map.getZoom() - settings.default_zoom) < 0.2) {
            first_fly = false;
            zoom_subscription.unsubscribe();
        }
    })
    function flyTo(map, coords) {
        let args = {
            center: [
                coords[1],
                coords[0]
            ],
            essential: !0,
        }
        if (first_fly || settings.reset_zoom) {
            args["zoom"] = settings.default_zoom;
        }
        map.flyTo(args, {flying: true});
    }
	// Proxy the map resetting
	map.state.flyTo = new Proxy(mapMethods.flyTo, {
		apply: (target, thisArg, args) => {
            // We're handling this ourselves below
            return;
		},
	});
    // Proxy the user interaction handling to not include flyTo calls
    ml_map.off("dragstart", mapMethods.handleUserInteraction);
    ml_map.off("zoomstart", mapMethods.handleUserInteraction);
    map.state.handleUserInteraction = new Proxy(mapMethods.handleUserInteraction, {
		apply: (target, thisArg, args) => {
            if (!args[0]?.flying) {
                return Reflect.apply(target, thisArg, args);
            }
		},
	});
    ml_map.on("dragstart", mapMethods.handleUserInteraction);
    ml_map.on("zoomstart", mapMethods.handleUserInteraction);
    // Sync map to the coordinates when the marker is updated
    // but only if the marker moved a significant distance (compared to the tile size)
    map.data.marker.setLngLat = new Proxy(map.data.marker.setLngLat, {
        apply: (target, thisArg, args) => {
            const map_lnglat = ml_map.getCenter();
            diff = [
                Math.abs(args[0][0] - map_lnglat.lng),
                Math.abs(args[0][1] - map_lnglat.lat)
            ]
            const tile_width = 360/(2**ml_map.getZoom());
            const factor = 0.01;
            if (
                (diff[0] > tile_width*factor || diff[1] > tile_width*factor)
                && (Date.now() - map.data.lastUserInteraction > 30000)
                && (settings.timeout_centre || map.data.lastUserInteraction == 0)
            ) {
                flyTo(ml_map, [args[0][1], args[0][0]])
            }
            return Reflect.apply(target, thisArg, args);
        }
    })

    // Add buttons to the map - define the Control object that holds them
    const contexts = ["Side", "Map", "Car", "Marker"];
    class TricksControl {
        constructor() {
            this._c_cont = document.createElement('div'); // Control container

            this._m_cont = document.createElement('div'); // Menu container
            this._m_cont.id = "mmt-menu";
            this._m_cont.style.display = "none";
            miniMapEl.appendChild(this._m_cont);
            document.addEventListener("click", (e) => {
                this._hide_menu();
            });

            this._m_options = document.createElement('div');
            this._m_options.className = 'maplibregl-ctrl maplibregl-ctrl-group';
            this._m_cont.appendChild(this._m_options);

            let 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');
            this._m_label.innerText = "Map";

            let label =  document.createElement('span');
            label.innerText = " menu";
            label.prepend(this._m_label);
            label_box.appendChild(label);

            let 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.classList.add("mmt-map-menu-opened");
        }
        _hide_menu() {
            this._m_cont.style.display = "none";
            mapContainerEl.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();
        }

        _context = undefined;
        set context(value) {
            this._m_label.innerText = value;
            this._m_cont.className = `mmt-menu-${value.replaceAll(' ', '-')}`
            this._context = value;
        }
        get context() {
            return this._context;
        }
        lat = 0;
        lng = 0;
        data = undefined;

        onAdd(map) {
            this._map = map;
            this._c_cont.className = 'maplibregl-ctrl maplibregl-ctrl-group';
            return this._c_cont;
        }

        onRemove() {
            this._c_cont.parentNode.removeChild(this._c_cont);
            this._map = undefined;
        }

        addButton(icon, name, callback, context=undefined) {
            // Add side button
            if (context == undefined || context.includes("Side")) {
                let button = document.createElement("button");
                button.style.display = settings[`side_${name}`] ? "block" : "none";
                button.title = name;
                add_checkbox(`Show ${name}`, `side_${name}`, (value) => {
                    button.style.display = value ? "block" : "none";
                }, this._s_cont);

                let 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 = () => {
                    this.context = "Side";
                    this.lat = vcontainer.data.currentCoords.lat;
                    this.lng = vcontainer.data.currentCoords.lng;
                    this.marker = undefined;
                    callback(this)
                };
                this._c_cont.appendChild(button);
            }
            
            let button = document.createElement("button");
            if (context !== undefined) {
                contexts.forEach((v, i) => {
                    if (!context.includes(v)) button.classList.add(`mmt-hide-${v.replaceAll(' ', '-')}`);
                })
            }

            let 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);

            let button_label = document.createElement("span");
            button_label.innerText = name;
            button.appendChild(button_label);

            button.onclick = () => {
                callback(this)
            };
            this._m_options.appendChild(button);

            return button_label;
        }
    }
    // Define map controls to add buttons for
    let control = new TricksControl();

    // Expose some APIs
    unsafeWindow._MMT_control = control;
    unsafeWindow._MMT_addContext = (name, available) => {
        contexts.push(name);
        const css_name = name.replaceAll(' ', '-');
        Array.from(control._m_options.children).forEach((child) => {
            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 all the buttons!
    // Go to 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 coordinates",
        async (c) => {
            let converted;
            try {
                converted = convert(prompt("Input coordinates here:"));
            } catch {
                alert("Coordinates were incorrect!");
                return
            }
            add_marker(converted.decimalLatitude, converted.decimalLongitude);
            flyTo(
                ml_map,
                [converted.decimalLatitude, converted.decimalLongitude]
            )
        },
        ["Side", "Map"]
    );

    // 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 = convert(`${c.lat},${c.lng}`).toCoordinateFormat("DMS").replaceAll(" ", "").replace(",", ", ");
            } else {
                coords = `${c.lat}, ${c.lng}`;
            }
            navigator.clipboard.writeText(coords);
        }
    );

    // Measure disctance
    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);
        }
    )

    // Open Street View
    control.addButton(
        "https://storage.googleapis.com/support-kms-prod/SNP_E2308F5561BE1525D2C88838252137BC5634_4353424_en_v0",
        "Open Street View",
        async (c) => {
            let data = vcontainer.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");
        }
    );

    // 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 url = (
                "https://sv-map.netlify.app/#base=roadmap&cov=all&" +
                `panos=&zoom=${ml_map.getZoom()+1}&center=${c.lat}%2C${c.lng}`
            )
		    window.open(url, "_blank")
        }
    );

    // Set up markers
    const markers = {};
    unsafeWindow._MMT_getMarkers = () => {
        return markers;
    }
    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";
    async function add_marker(lat, lng, marker_id=undefined, color=undefined) {
        const marker = new maplibre.Marker({
            draggable: true,
            opacity: 0.7,
            scale: 0.8,
            color: color ? color : settings.marker_color
        })
          .setLngLat([lng, lat])
          .addTo(ml_map);

        if (!marker_id) {
            marker_id = crypto.randomUUID();
            settings.markers[marker_id] = [lat, lng];
            GM.setValues(settings);
        }
        marker._mmt_id = marker_id;
        markers[marker_id] = marker;

        marker._mmt_remove = () => {
            delete settings.markers[marker_id];
            delete markers[marker_id];
            GM.setValues(settings);
            marker.remove();
        }

        marker.on("dragend", (e) => {
            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
            );
        });
    }
    for (const [marker_id, value] of Object.entries(settings.markers)) {
        add_marker(value[0], value[1], marker_id, value[2]);
    }

    // Add marker
    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"]
    );

    // Centre
    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(
                ml_map,
                [c.lat, c.lng]
            )
            if (c.context == "Side" || c.context == "Car") {
                map.state.lastUserInteraction = 0;
            }
        },
        ["Side", "Car", "Marker"]
    );

    // 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",
        (c) => {mcol_input.click()},
        ["Marker"]
    )
    const mcol_input = document.createElement("input");
    mcol_input.type = "color";
    mcol_input.id = "mmt-menu-color";
    mcol_input.addEventListener("input", (e) => {
        if (control.data) {
            control.data.getElement().children[0].children[0].children[1].setAttribute(
                "fill", mcol_input.value
            );
            settings.markers[control.data._mmt_id][2] = mcol_input.value;
            GM.setValues(settings);
        }
    })

    // Remove marker
    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) => {
            control.data._mmt_remove();
        },
        ["Marker"]
    );

    // Add the Control to the map and set up triggers for contex menus
    ml_map.addControl(control, "bottom-left");
    ml_map.on("contextmenu", (e) => {
        control.openMenu(
            "Map", e.lngLat.lat, e.lngLat.lng,
            e.originalEvent.clientX, e.originalEvent.clientY
        )
    })
    marker_el.oncontextmenu = (e) => {
        e.stopPropagation();
        e.preventDefault();

        control.openMenu(
            "Car",
            vcontainer.data.currentCoords.lat,
            vcontainer.data.currentCoords.lng,
            e.clientX, e.clientY
        )
    }
    // Hide the menu when PIP exits
    let inPIP = false;
    if (window.documentPictureInPicture) {
        documentPictureInPicture.addEventListener("enter", (e) => {
            setLayerOpacity(1);
            inPIP = true;
            e.window.addEventListener("pagehide", (f) => {
                control._hide_menu();
                setLayerOpacity();
                inPIP = false;
            });
        })
    }

    // Add a scale bar
    const scale_control = new (await IRF.modules.maplibre).ScaleControl({
        unit: odometer.data.isKilometers ? "metric": "imperial"
    })
    ml_map.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(odometer.state, 'isKilometers');
    // Override the setter
    Object.defineProperty(odometer.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
            if (measure) measure.setDistance();
            return r_value
        },
        configurable: true,
        enumerable: true,
    });

    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) {
        odometer.state.isKilometers = true;
    }
    add_checkbox("Use metric units", "km_units", async (value) => {
        odometer.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);
    (await IRF.vdom.container).state.updateData = new Proxy(
        (await IRF.vdom.container).methods.updateData, {
        apply: (target, thisArg, args) => {
            // debugger;
            let distance = args[0]["distance"]
            if (odometer.data.isKilometers) {distance *= odometer.data.conversionFactor}
            const decimals = (distance % 1).toFixed(2);
            decimal_el.innerHTML = `<br>${decimals.substring(1)}`;
            return Reflect.apply(target, thisArg, args);
        },
    });
    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);

    add_checkbox("Use minutes and seconds for coordinates", "coordinates_fancy");

    // Opacities
    function setLayerOpacity(value=undefined) {
        if (!value) {
            value = map.data.isExpanded ? settings.background_opacity_expanded : settings.background_opacity;
        }
        value = parseFloat(value);
        ml_map.setPaintProperty("background", "background-opacity", value);
        ml_map.setPaintProperty("water", "fill-opacity", value);
    }
    // Map opacity
    mapContainerEl.style.opacity = settings.map_opacity;
    add_slider("Collapsed map opacity", "map_opacity", (value) => {
        mapContainerEl.style.opacity = value;
    }, [0, 1, 0.05]);
    add_slider("Collapsed map background opacity", "background_opacity", (value) => {
        if (!map.data.isExpanded) setLayerOpacity(value);
    }, [0, 1, 0.05]);
    mapContainerEl.style.setProperty('--map-opacity-expanded', settings.map_opacity_expanded);
    add_slider("Expanded map opacity", "map_opacity_expanded", (value) => {
        mapContainerEl.style.setProperty('--map-opacity-expanded', value);
    }, [0, 1, 0.05]);
    add_slider("Expanded map background opacity", "background_opacity_expanded", (value) => {
        if (map.data.isExpanded) setLayerOpacity(value);
    }, [0, 1, 0.05]);

    //Marker opacity
    marker_el.style.setProperty('--marker-opacity', settings.marker_opacity);
    // Route opacity
    add_slider("Route opacity", "route_opacity", (value) => {
        ml_map.setPaintProperty("route", "line-opacity", parseFloat(value));
        ml_map.setPaintProperty("old-route-layer", "line-opacity", parseFloat(value));
    }, [0, 1, 0.05]);

    // 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), 100)}vw` : "");
        miniMapEl.style.setProperty('--map-height', height ? `${Math.min(Math.max(0, height), 100)}vh` : "");
        miniMapEl.style.setProperty('--map-width-expanded', expanded_width ? `${Math.min(Math.max(0, expanded_width), 100)}vw` : "");
        miniMapEl.style.setProperty('--map-height-expanded', expanded_height ? `${Math.min(Math.max(0, expanded_height), 100)}vh` : "");
    }

    // Handle the dragging logic for resizing
    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;

    expandButtonEl.addEventListener('mousedown', (e) => {
        isClicked = true;
        lastX = e.clientX;
        lastY = e.clientY;
        e.preventDefault();
    });

    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.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;
    });

    document.addEventListener('mouseup', (e) => {
        isClicked = false;
    });

    // Override toggleExpand, which always fires on mouseup on the expand button
    map.state.toggleExpand = new Proxy(map.methods.toggleExpand, {
        apply(ogToggleExpand, thisArg, args) {
            if (isResizing) {
                isResizing = false;
                return;
            }
            isClicked = false;
            const value = ogToggleExpand.apply(thisArg, args);
            setLayerOpacity();
            return value;
        }
    });

    setMiniMapSize(settings.map_size);
    ml_map.resize();

    // Automatically expand the map
    if (window.innerWidth > 900 && settings.expand_map) {
        map.state.isExpanded = true;
    }
    // Perform some actions once the map loads
    ml_map.on('load', () => {
        // Redraw when loaded, as map.state.isExpanded is not immediate
        ml_map.resize();

        // Messing with styles should only happen once map is ready
        ml_map.setPaintProperty("route", "line-opacity", parseFloat(settings.route_opacity));
        setLayerOpacity();
        mapContainerEl.addEventListener("mouseenter", (e) => {
            if (inPIP) return;
            setLayerOpacity(1);
        });
        mapContainerEl.addEventListener("mouseleave", (e) => {
            if (inPIP) return;
            setLayerOpacity();
        });

        // Add coverage
        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",
                }
            }, "route"
        );
        ml_map.addLayer(
            {
                id: 'svugc-tiles',
                type: 'raster',
                source: 'ugc_sv',
                minzoom: 0,
                maxzoom: 22,
                layout: {
                    visibility: settings.coverage ? "visible" : "none",
                }
            }, "route"
        );
        ml_map.moveLayer("route", "highway-name-path");
        ml_map.moveLayer("svugc-tiles", "route");
        ml_map.moveLayer("sv-tiles", "svugc-tiles");
        ml_map.setPaintProperty("svugc-tiles", "raster-opacity", parseFloat(settings.coverage_opacity));
        ml_map.setPaintProperty("sv-tiles", "raster-opacity", parseFloat(settings.coverage_opacity));
    });
    // Set the old route opacity once it's added
    const old_route_subscription = ml_map.on('data', "old-route-layer", (e) => {
        if (e.sourceID = "old-route-layer") {
            ml_map.setPaintProperty("old-route-layer", "line-opacity", parseFloat(settings.route_opacity));
            ml_map.moveLayer("old-route-layer", "route");
            ml_map.moveLayer("svugc-tiles", "old-route-layer");
            ml_map.moveLayer("sv-tiles", "svugc-tiles");
            old_route_subscription.unsubscribe();
        }
    })

    // Marker colour GUI setting
    {
        let label = document.createElement("label");

        let input = document.createElement("input");
        input.type = "color";
        input.value = settings.marker_color;
        label.appendChild(input);

        let text = document.createElement("span");
        text.innerText = " Added marker colour (right-click map to add marker)";
        label.appendChild(text);

        input.onchange = () => {
            settings.marker_color = input.value;
            GM.setValues(settings);
        }

        irf_settings.container.appendChild(label);
        irf_settings.container.appendChild(document.createElement("br"));
        irf_settings.container.appendChild(document.createElement("br"));
    }

    // Correct car marker offset
    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`;

    // 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);

    // Correct the marker rotation when the car moves
    const changeStop = (await IRF.vdom.container).methods.changeStop;
    vcontainer.state.changeStop = new Proxy(changeStop, {
		apply: (target, thisArg, args) => {
			const returnValue = Reflect.apply(target, thisArg, args);
            map.data.marker.setRotation(args[3]);
            let 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)`;
            }
            if (measure) measure.updateCar();
            return returnValue;
		},
	});
    // Override the normal marker rotation settings, we do it above!
    map.state.setMarkerRotation = new Proxy(mapMethods.setMarkerRotation, {
        apply: (target, thisArg, args) => {}
    });

    // Custom car marker settings
    irf_settings.container.appendChild(document.createElement("hr"));
    irf_settings.container.appendChild(document.createElement("br"));

    add_slider("Car marker opacity", "marker_opacity", (value) => {
        marker_el.style.setProperty('--marker-opacity', value);
    }, [0, 1, 0.05]);

    add_checkbox("Custom car marker", "car_marker_custom", (show) => {
        custom_car.style.display = show ? "block" : "none";
        marker_el.style.backgroundImage = default_marker_svg();
    })

    // URL for car marker
    {
        let label = document.createElement("label");

        let text = document.createElement("span");
        text.innerHTML = " Custom car marker image URL<br> (host on <a href='https://catbox.moe' target='_blank'>catbox.moe</a>):";
        label.appendChild(text);
        label.appendChild(document.createElement("br"));

        var box = document.createElement("input");
        box.value = settings.car_marker_url;
        box.style.width = "100%";
        label.appendChild(box);

        irf_settings.container.appendChild(label);
        irf_settings.container.appendChild(document.createElement("br"));
        irf_settings.container.appendChild(document.createElement("br"));

        box.oninput = () => {
            settings.car_marker_url = box.value;
            GM.setValues(settings);
            custom_car.src = settings.car_marker_url;
        }
    }
    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]);
    add_slider("Custom car marker scale (%)", "car_marker_scale", (value) => {
        custom_car.style.maxWidth = `${value}%`;
        custom_car.style.maxHeight = `${value}%`;
    }, [0, 100, 1]);
    add_slider("Custom car marker rotation (deg)", "car_marker_rotation", (value) => {
        custom_car.style.rotate = `${value}deg`;
    }, [0, 360, 5]);

    add_checkbox("Flip image", "car_marker_flip")
    add_checkbox("Flip image when going left", "car_marker_flip_x")

    const attribution = document.createElement("span");
    attribution.innerHTML = "Default white van picture:<br><a href='https://www.vecteezy.com/free-png/2d-delivery-truck-top-view' target='_blank'>2d Delivery Truck Top View PNGs by Vecteezy</a> - "
    irf_settings.container.appendChild(attribution);
    const restore_van = document.createElement("span");
    restore_van.innerHTML = "Restore"
    restore_van.onclick = () => {
        box.value = "https://files.catbox.moe/a55qk5.png";
        box.dispatchEvent(new Event("input"));
    }
    restore_van.style.textDecoration = "underline";
    restore_van.style.color = "#aaa";
    restore_van.style.cursor = "pointer";
    irf_settings.container.appendChild(restore_van);
    irf_settings.container.appendChild(document.createElement("br"));
    irf_settings.container.appendChild(document.createElement("br"));

    // Map side menu settings
    irf_settings.container.appendChild(document.createElement("hr"));
    irf_settings.container.appendChild(document.createElement("br"));
    const menu_s_label = document.createElement("span");
    menu_s_label.innerText = "Minimap side buttons:";
    irf_settings.container.appendChild(menu_s_label);
    irf_settings.container.appendChild(document.createElement("br"));
    irf_settings.container.appendChild(document.createElement("br"));

    // Compass first
    // Add a compass
    const compass = new maplibre.NavigationControl({
        visualizePitch: true,
        visualizeRoll: true,
        showCompass: true,
        showZoom: false
    })
    ml_map.addControl(compass, "bottom-left");
    compass._container.style.display = settings.side_compass ? "block" : "none";
    add_checkbox("Show compass", "side_compass", (show) => {
        compass._container.style.display = show ? "block" : "none";
    })

    // Then all the other buttons
    irf_settings.container.appendChild(control._s_cont);

    // Coverage settings
    irf_settings.container.appendChild(document.createElement("hr"));
    irf_settings.container.appendChild(document.createElement("br"));
    add_checkbox("Show coverage", "coverage", (value) => {
        ["svugc-tiles", "sv-tiles"].forEach((kind) => {
            ml_map.setLayoutProperty(
                kind, "visibility",
                settings.coverage ? "visible" : "none"
            )
        });
    })
    add_slider("Coverage opacity", "coverage_opacity", (value) => {
        ["svugc-tiles", "sv-tiles"].forEach((kind) => {
            ml_map.setPaintProperty(kind, "raster-opacity", parseFloat(settings.coverage_opacity));
        });
    }, [0, 1, 0.05])

    // Measure distances
    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>';
    class DistanceControl {
        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.addControl(distance_control, "top-left");

    // Code for measuring distance is heavily rewritten into an object
    // https://maplibre.org/maplibre-gl-js/docs/examples/measure/

    // This object should handle most of the abstract measuring functions
    class Measure {
        // GeoJSON object to hold our measurement features - points and a line
        geojson_points = {
            'type': 'FeatureCollection',
            'features': []
        };
        geojson_line = {
            'type': 'FeatureCollection',
            'features': []
        };

        // Feature to draw the line between points
        linestring = {
            'type': 'Feature',
            'geometry': {
                'type': 'LineString',
                'coordinates': []
            }
        };

        car = undefined;
        toggleCar() {
            if (this.car) {
                this.removePoint(this.car);
                this.car = undefined;
            } else {
                this.addPoint(
                    vcontainer.data.currentCoords.lat,
                    vcontainer.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.data.currentCoords.lng,
                vcontainer.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 = odometer.data.isKilometers ? "km" : "mi";
            const conversion = odometer.data.isKilometers ? 1 : odometer.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.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.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.fitBounds(
                (await ml_map.getSource('geojson_points').getBounds()),
                {
                    padding: 50
                }
            )
        }
    }
    measure = new Measure();

    ml_map.on('load', () => {
        // Add the two data sources
        ml_map.addSource('geojson_line', {
            'type': 'geojson',
            'data': measure.geojson_line
        });
        ml_map.addSource('geojson_points', {
            'type': 'geojson',
            'data': measure.geojson_points
        });

        // Add layers and styles to the map
        ml_map.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.addLayer({
            id: 'measure-points',
            type: 'circle',
            source: 'geojson_points',
            paint: {
                'circle-radius': 5,
                'circle-color': '#000b'
            },
        });
        ml_map.moveLayer("measure-lines");
        ml_map.moveLayer("measure-points");

        // Handle clicking
        ml_map.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.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);
            }
        });

        marker_el.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.on('mousemove', (e) => {
            if (!distance_control.check.checked) {
                // If we're not editing the measurements, stick to the default grab
                ml_map.getCanvas().style.cursor = "grab";
            } else {
                const features = ml_map.queryRenderedFeatures(e.point, {
                    layers: ['measure-points']
                });
                // Pointer if hovering over a point, crosshair otherwise
                ml_map.getCanvas().style.cursor = features.length ?
                    'pointer' :
                    'crosshair';
            }
        });
    });


})();