// ==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 // @downloadURL none // ==/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; } `);