Geoguessr Like Games - Location Finder

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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