Geoguessr Like Games - Location Finder

Pinpointing location finder (Geoguessr, Geotastic, GeoHub, etc.). CORS Unlocker extension needed.

当前为 2024-12-07 提交的版本,查看 最新版本

// ==UserScript==
// @name         Geoguessr Like Games - Location Finder
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Pinpointing location finder (Geoguessr, Geotastic, GeoHub, etc.). CORS Unlocker extension needed.
// @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==

(function () {
   "use strict";

   let alertOffset = 0;
   let geocodingApiKey = loadData("geocodingApiKey");
   if (!geocodingApiKey) editGeocodingApiKey();

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