Geoguessr Like Games - Location Finder

Pinpointing location finder (Geoguessr, Geotastic, GeoHub, etc.).

// ==UserScript==
// @name         Geoguessr Like Games - Location Finder
// @namespace    http://tampermonkey.net/
// @version      2.0.1
// @description  Pinpointing location finder (Geoguessr, Geotastic, GeoHub, etc.).
// @author       Meffiu
// @match        https://geotastic.net/*
// @match        https://www.geoguessr.com/*
// @match        https://www.geohub.gg/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geotastic.net
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(async function () {
    "use strict";

    // Vars
    let alertOffset = 0;
    let geocodingApiKey = loadData("geocodingApiKey");
    if (!geocodingApiKey) editGeocodingApiKey();
    let flagIDs = await loadFlagIDs();
    let defaultDescription =
        "Press search button to find location.<br>Press key button to edit Geocoding API key. (geocode.maps.co)<br>Press refresh button if searching is stuck on same location.";

    // Menu
    const style = document.createElement("style");
    style.innerHTML = `
        #dynamicMenu {
            position: fixed;
            bottom: -300px;
            left: 40px;
            width: 400px;
            background-color: #f4f4f4;
            box-shadow: 0 -2px 5px rgba(0,0,0,0.5);
            transition: bottom 0.3s ease;
            z-index: 10000;
            border-radius: 10px 10px 0 0;
        }
        #dynamicMenu.open {
            bottom: 0;
        }
        #menuTitle {
            background-color: #4CAF50;
            color: white;
            padding: 15px;
            font-size: 18px;
            text-align: left;
            border-radius: 10px 10px 0 0;
            cursor: pointer;
        }
        #menuDescription {
            padding: 15px;
            font-size: 14px;
            color: #333;
            text-align: left;
        }
        #toggleMenu {
            position: fixed;
            bottom: 10px;
            left: 150px;
            transform: translateX(-50%);
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 15px;
            font-size: 16px;
            cursor: pointer;
            z-index: 10001;
            border-radius: 20px;
        }
        #toggleMenu:hover {
            background-color: #45a049;
        }
        #searchIcon, #keyIcon, #refreshIcon {
            position: absolute;
            top: 10px;
            width: 24px;
            height: 24px;
            cursor: pointer;
        }
        #searchIcon {
            right: 10px;
        }
        #keyIcon {
            right: 40px;
        }
        #refreshIcon {
            right: 70px;
        }
    `;
    document.head.appendChild(style);

    const toggleButton = document.createElement("button");
    toggleButton.id = "toggleMenu";
    toggleButton.textContent = `Location Finder v${GM_info.script.version}`;
    document.body.appendChild(toggleButton);

    const menu = document.createElement("div");
    menu.id = "dynamicMenu";

    const title = document.createElement("div");
    title.id = "menuTitle";
    title.textContent = `Location Finder v${GM_info.script.version}`;

    const searchIcon = document.createElement("img");
    searchIcon.id = "searchIcon";
    searchIcon.src = "https://img.icons8.com/FFFFFF/452/search--v1.png";
    title.appendChild(searchIcon);

    const keyIcon = document.createElement("img");
    keyIcon.id = "keyIcon";
    keyIcon.src = "https://img.icons8.com/FFFFFF/452/key--v1.png";
    title.appendChild(keyIcon);

    const refreshIcon = document.createElement("img");
    refreshIcon.id = "refreshIcon";
    refreshIcon.src = "https://img.icons8.com/win10/FFFFFF/452/refresh--v1.png";
    title.appendChild(refreshIcon);

    searchIcon.addEventListener("click", (event) => {
        event.stopPropagation();
        getLocation();
    });

    keyIcon.addEventListener("click", (event) => {
        event.stopPropagation();
        editGeocodingApiKey();
    });

    refreshIcon.addEventListener("click", (event) => {
        event.stopPropagation();
        performance.clearResourceTimings();
        log("Performance resource timings cleared.");
        showAlert(
            "Refreshed",
            "Performance resource timings has been cleared.\nTry to move around and press search button.",
            "green",
            2000
        );
    });

    const description = document.createElement("div");
    description.id = "menuDescription";
    description.innerHTML = defaultDescription;

    menu.appendChild(title);
    menu.appendChild(description);
    document.body.appendChild(menu);

    toggleButton.addEventListener("click", () => {
        menu.classList.add("open");
        toggleButton.style.display = "none";
    });

    title.addEventListener("click", () => {
        menu.classList.remove("open");
        toggleButton.style.display = "block";
    });

    // Functions
    function log(message) {
        if (message.startsWith("Error:")) {
            console.error(`[Location Finder v${GM_info.script.version}]\n${message}`);
        } else {
            console.log(`[Location Finder v${GM_info.script.version}]\n${message}`);
        }
    }

    function saveData(key, data) {
        GM_setValue(key, JSON.stringify(data));
    }

    function loadData(key) {
        const data = GM_getValue(key, null);
        return data ? JSON.parse(data) : null;
    }

    function editGeocodingApiKey() {
        const key = prompt("Please enter your Geocoding API key:");
        if (key) {
            geocodingApiKey = key;
            saveData("geocodingApiKey", key);
            log(`API key set to ${key}`);
        }
    }

    function handleError(error) {
        log(`Error:\n${error.stack}`);
        if (
            error.stack.includes("Failed to fetch") &&
            error.stack.includes("at getLatestGeoPhotoService")
        ) {
            alert(
                "CORS is not unlocked!\nDownload an extension to unlock (eg. CORS Unlocker)\n(Or unlock it by any other method)"
            );
        }
    }

    async function getLocation() {
        description.innerHTML = "Searching...";
        if (
            window.location.href.includes("geotastic.net") &&
            document.querySelector(".flag-icon")
        ) {
            const flagID = document
                .querySelector(".flag-icon img")
                .getAttribute("src")
                .split("/")[4]
                .split(".")[0];
            const country = flagIDs[flagID];
            description.innerHTML = "";
            const countryElement = document.createElement("h2");
            countryElement.style.display = "flex";
            countryElement.style.alignItems = "center";
            const flagImg = document.createElement("img");
            flagImg.src = `https://static.infra.geotastic.net/flags_rect/${flagID}.svg`;
            flagImg.alt = `${country} flag`;
            flagImg.style.marginLeft = "10px";
            flagImg.style.width = "32px";
            countryElement.textContent = country;
            countryElement.append(flagImg);
            description.appendChild(countryElement);
            return;
        }
        const geoBody = await getLatestGeoPhotoService();
        if (!geoBody) return;
        log("Got GeoPhotoService response.");

        const location = lonlatExtraction(geoBody);
        log(`Lat & Lon: ${location}`);
        const address = await getAddress(location);

        if (address) {
            description.innerHTML = "";

            if (address.country) {
                const countryElement = document.createElement("h2");
                countryElement.style.display = "flex";
                countryElement.style.alignItems = "center";
                const flagImg = document.createElement("img");
                flagImg.src = `https://flagsapi.com/${address.country_code.toUpperCase()}/flat/32.png`;
                flagImg.alt = `${address.country} flag`;
                flagImg.style.marginLeft = "10px";
                countryElement.textContent = address.country;
                countryElement.append(flagImg);
                description.appendChild(countryElement);
            }

            const ul = document.createElement("ul");

            for (let key in address) {
                if (key.includes("ISO") || key === "country_code" || key === "country") {
                    continue;
                }

                const li = document.createElement("li");
                li.textContent = `${key}: ${address[key]}`;
                ul.appendChild(li);
            }

            description.appendChild(ul);

            const button = document.createElement("button");
            button.textContent = "Open in Google Maps";
            button.style.display = "block";
            button.style.margin = "10px 0";
            button.style.padding = "5px 10px";
            button.style.fontSize = "16px";
            button.style.color = "#fff";
            button.style.backgroundColor = "#4CAF50";
            button.style.border = "none";
            button.style.borderRadius = "5px";
            button.style.cursor = "pointer";
            button.style.transition = "background-color 0.3s ease";

            button.addEventListener("mouseover", () => {
                button.style.backgroundColor = "#45a049";
            });

            button.addEventListener("mouseout", () => {
                button.style.backgroundColor = "#4CAF50";
            });

            button.addEventListener("click", () => {
                window.open(`https://maps.google.com/?q=${location[0]},${location[1]}`, "_blank");
            });

            description.appendChild(button);
            performance.clearResourceTimings();
        }
    }

    async function getLatestGeoPhotoService() {
        const performanceEntries = await performance.getEntriesByType("resource");

        let lastGeoPhotoServiceRequest = null;

        for (let i = performanceEntries.length - 1; i >= 0; i--) {
            if (performanceEntries[i].name.includes("GeoPhotoService.GetMetadata")) {
                lastGeoPhotoServiceRequest = performanceEntries[i];
                break;
            }
        }

        if (lastGeoPhotoServiceRequest) {
            const geoBody = await fetch(lastGeoPhotoServiceRequest.name)
                .then((response) => response.text())
                .catch((error) => handleError(error));
            return geoBody;
        } else {
            log("No GeoPhotoService request found.");
            showAlert(
                "No GeoPhotoService request found",
                "1. Make sure you are in Street View game.\n2. Move around a few times before searching.",
                "red",
                5000
            );
            description.innerHTML = defaultDescription;
            return null;
        }
    }

    function lonlatExtraction(body) {
        const regex = /-?\d+\.\d+/g;
        const matches = body.match(regex);

        const lonlat = matches.slice(0, 2);
        return lonlat;
    }

    async function getAddress([lat, lon]) {
        const url = `https://geocode.maps.co/reverse?lat=${lat}&lon=${lon}&api_key=${geocodingApiKey}`;
        const response = await fetch(url)
            .then((response) => response.json())
            .catch((error) => handleError(error));

        log(`Address:\n${JSON.stringify(response)}`);

        if (response) {
            return response.address;
        } else {
            log(`No response from Geocoding API.`);
            showAlert(
                "No response from Geocoding API",
                "Make sure your api key is correct.",
                "red",
                5000
            );
            return null;
        }
    }

    function showAlert(title, description, color, duration) {
        const alertBox = document.createElement("div");

        alertBox.style.position = "fixed";
        alertBox.style.top = `${20 + alertOffset}px`;
        alertBox.style.right = "20px";
        alertBox.style.padding = "15px";
        alertBox.style.color = "white";
        alertBox.style.backgroundColor = color;
        alertBox.style.maxWidth = "400px";
        alertBox.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
        alertBox.style.borderRadius = "10px";
        alertBox.style.zIndex = "1000";
        alertBox.style.opacity = "0";
        alertBox.style.transition = "opacity 0.5s ease, transform 0.5s ease";
        alertBox.style.transform = "translateY(-20px)";

        const alertTitle = document.createElement("div");
        alertTitle.textContent = title;
        alertTitle.style.fontWeight = "bold";
        alertTitle.style.fontSize = "16px";
        alertTitle.style.marginBottom = "5px";

        const alertDescription = document.createElement("div");
        alertDescription.innerHTML = description.replace(/\n/g, "<br>");
        alertDescription.style.fontSize = "14px";

        alertBox.appendChild(alertTitle);
        alertBox.appendChild(alertDescription);

        document.body.appendChild(alertBox);

        requestAnimationFrame(() => {
            alertBox.style.opacity = "1";
            alertBox.style.transform = "translateY(0)";
        });

        setTimeout(() => {
            alertBox.style.opacity = "0";
            alertBox.style.transform = "translateY(-20px)";
            setTimeout(() => {
                document.body.removeChild(alertBox);
                alertOffset -= 90;
            }, 500);
        }, duration);

        alertOffset += 90;
    }

    async function loadFlagIDs() {
        const url =
            "https://gist.githubusercontent.com/Meff1u/1e596b84c8772355636326cc422a9fd0/raw/c6bf24bda96ce457b715688a692006c01807159c/flags.json";
        const response = await fetch(url)
            .then((response) => response.json())
            .catch((error) => handleError(error));

        if (response) {
            return response;
        } else {
            return null;
        }
    }

    function antiAdblockSkip() {
        const skipButton = document.querySelector(".ad-blocker-info button");
        if (skipButton) {
            skipButton.removeAttribute("disabled");
            skipButton.classList.remove("v-btn-disabled");
            skipButton.click();
        }
    }

    // MutationObserver
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach((node) => {
                    if (
                        node.nodeType === Node.ELEMENT_NODE &&
                        node.tagName.toLowerCase() === "div" &&
                        node.getAttribute("role") === "dialog"
                    ) {
                        antiAdblockSkip();
                    }
                });
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();