Greasy Fork

Guess Peek (Geoguessr)

See where your guess was after each round!

目前为 2024-01-16 提交的版本。查看 最新版本

// ==UserScript==
// @name         Guess Peek (Geoguessr)
// @namespace    alienperfect
// @version      1.2
// @description  See where your guess was after each round!
// @author       Alien Perfect
// @match        https://www.geoguessr.com/*
// @icon         https://www.google.com/s2/favicons?sz=32&domain=geoguessr.com
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_info
// @grant        unsafeWindow
// @grant        window.onurlchange
// ==/UserScript==

"use strict";

let svs;
let gameId;
let round;
let roundCount;

const SEARCH_RADIUS = 250000;
const SCRIPT_NAME = GM_info.script.name;
const GAMES_API = "https://www.geoguessr.com/api/v3/games/";

const markerObserver = new MutationObserver(() => {
  const map = document.querySelector("[class*='coordinate-result-map']");
  const marker = document.querySelector("[data-qa='guess-marker']");

  if (map && marker) {
    markerObserver.disconnect();

    const pano = JSON.parse(sessionStorage.getItem(`${gameId}-${round}`));

    updateMarker(marker, pano);
  }
});

const markerListObserver = new MutationObserver(async () => {
  const playAgain = document.querySelector("[data-qa='play-again-button']");
  const markerList = document.querySelectorAll("[data-qa='guess-marker']");

  if (playAgain && markerList.length > 0) {
    markerListObserver.disconnect();

    for (let i = 1; i <= roundCount; i++) {
      const marker = markerList.item(i - 1);
      const pano = JSON.parse(sessionStorage.getItem(`${gameId}-${i}`));

      updateMarker(marker, pano);
    }
  }
});

function main() {
  console.log(`${SCRIPT_NAME} is running!`);

  interceptFetch();

  window.addEventListener("urlchange", () => {
    markerObserver.disconnect();
    markerListObserver.disconnect();
  });
}

function interceptFetch() {
  const _fetch = unsafeWindow.fetch;

  unsafeWindow.fetch = async (resource, options) => {
    const response = await _fetch(resource, options);
    const url = typeof resource === "string" ? resource : resource.url;

    if (url.includes(GAMES_API) && options.method === "POST") {
      try {
        const resp = await response.clone().json();
        const guessList = resp.player.guesses;
        const guess = guessList.at(-1);
        const guessCoords = { lat: guess.lat, lng: guess.lng };
        const gameFinished = resp.state === "finished";
        const pano = await getNearestPano(guessCoords);

        gameId = resp.token;
        round = resp.round;
        roundCount = resp.roundCount;
        sessionStorage.setItem(`${gameId}-${round}`, JSON.stringify(pano));

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

        if (gameFinished) {
          await syncGuesses(guessList);

          markerListObserver.observe(document.body, {
            childList: true,
            subtree: true,
          });
        }
      } catch (error) {
        console.error(`${SCRIPT_NAME} error: ${error}`);
      }
    }

    return response;
  };
}

async function getNearestPano(coords) {
  let pano;
  let oldRadius;
  let radius = SEARCH_RADIUS;

  if (!svs) initSVS();

  while (true) {
    try {
      pano = await svs.getPanorama({
        location: coords,
        radius: radius,
        source: "outdoor",
        preference: "nearest",
      });

      radius =
        unsafeWindow.google.maps.geometry.spherical.computeDistanceBetween(
          coords,
          pano.data.location.latLng,
        );

      pano.radius = radius;
      pano.url = getStreetViewUrl(pano.data.location.pano);

      if (oldRadius && radius >= oldRadius) break;

      oldRadius = radius;
    } catch (e) {
      break;
    }
  }

  return pano;
}

function updateMarker(marker, pano) {
  const distance = humanizeDistance(SEARCH_RADIUS);
  const tooltip = document.createElement("div");

  tooltip.className = "peek-tooltip";
  tooltip.textContent = `No location was found within ${distance}!`;

  if (pano) {
    const distance = humanizeDistance(pano.radius);

    tooltip.textContent = `Click to see the nearest location! [${distance}]`;
    
    marker.setAttribute("data-pano", "");
    marker.addEventListener("click", () => {
      window.open(pano.url, "_blank");
    });
  } else {
    marker.setAttribute("data-no-pano", "");
  }

  marker.append(tooltip);
}

async function syncGuesses(guessList) {
  for (let i = 1; i <= roundCount; i++) {
    let pano = JSON.parse(sessionStorage.getItem(`${gameId}-${i}`));

    if (!pano) {
      const coords = {
        lat: guessList[i - 1].lat,
        lng: guessList[i - 1].lng,
      };

      pano = await getNearestPano(coords);
      sessionStorage.setItem(`${gameId}-${i}`, JSON.stringify(pano));
    }
  }
}

function initSVS() {
  svs = new unsafeWindow.google.maps.StreetViewService();
}

function getStreetViewUrl(panoId) {
  return `https://www.google.com/maps/@?api=1&map_action=pano&pano=${panoId}`;
}

function humanizeDistance(distance) {
  if (distance >= 1000) return (distance / 1000).toFixed(1) + " km";
  return distance.toFixed(1) + " m";
}

main();

GM_addStyle(`
  .peek-tooltip {
    display: none;
    position: absolute;
    width: 120px;
    background: #323232;
    border-radius: 4px;
    text-align: center;
    padding: 0.5rem;
    font-size: 0.9rem;
    right: 50%;
    bottom: 220%;
    margin-right: -60px;
    opacity: 90%;
    z-index: 4;
  }

  .peek-tooltip:after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    margin-left: -5px;
    border-width: 5px;
    border-style: solid;
    border-color: #323232 transparent transparent transparent;
  }

  [data-pano]:hover .peek-tooltip,
  [data-no-pano]:hover .peek-tooltip {
    display: block;
  }

  [data-pano] > :first-child {
    cursor: pointer;
    --border-color: #E91E63 !important;
    --border-size-factor: 2 !important;
  }

  [data-no-pano] > :first-child {
    cursor: initial;
    --border-color: #323232 !important;
    --border-size-factor: 1.5 !important;
  }
`);