您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Preview your GeoGuessr guess before placing it!
// ==UserScript== // @name Guess Preview (GeoGuessr) // @namespace rawblocky // @version 2025.06.10 // @description Preview your GeoGuessr guess before placing it! // @author Rawblocky // @match *://*.geoguessr.com/* // @run-at document-start // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @grant window.onurlchange // @license MIT // ==/UserScript== // Credit to Alien Perfect's original Guess Peek const SETTINGS = { SEARCH_RADIUS: 50000, PREVIEW_SIZE_WIDTH: "30%", PREVIEW_SIZE_HEIGHT: "25%", EMBED_ON_HOVER: true, ENLARGE_ON_HOVER: true, HOVER_WIDTH: "70%", HOVER_HEIGHT: "70%", }; const KEYBINDINGS = { TOGGLE_PREVIEW: "x", }; GM_addStyle(` .guess-preview-button { position: absolute; bottom: 0; left: 0; width: ${SETTINGS.PREVIEW_SIZE_WIDTH}; height: ${SETTINGS.PREVIEW_SIZE_HEIGHT}; z-index: 10; user-select: none; } .guess-preview-button:hover { width: ${ (SETTINGS.ENLARGE_ON_HOVER && SETTINGS.HOVER_WIDTH) || SETTINGS.PREVIEW_SIZE_WIDTH }; height: ${ (SETTINGS.ENLARGE_ON_HOVER && SETTINGS.HOVER_HEIGHT) || SETTINGS.PREVIEW_SIZE_HEIGHT }; } `); let isPreviewEnabled = GM_getValue("isPreviewEnabled") || true; let svs; let latestCoords; function initSVS() { svs = new unsafeWindow.google.maps.StreetViewService(); } function convertDistance(distance) { if (distance >= 1000) return (distance / 1000).toFixed(1) + " km"; return distance.toFixed(1) + " m"; } function computeDistanceBetween(coords1, coords2) { return unsafeWindow.google.maps.geometry.spherical.computeDistanceBetween( coords1, coords2 ); } function getStreetViewUrl(panoId) { return `https://www.google.com/maps/@?api=1&map_action=pano&pano=${panoId}`; } async function getNearestPano(coords) { let pano = {}; let panorama, oldRadius; let radius = SETTINGS.SEARCH_RADIUS; if (!svs) initSVS(); while (true) { try { panorama = await svs.getPanorama({ location: coords, radius: radius, source: "outdoor", preference: "nearest", }); let roadHeading = 0; if (panorama.data.tiles && panorama.data.tiles.centerHeading) { roadHeading = panorama.data.tiles.centerHeading; } radius = computeDistanceBetween(coords, panorama.data.location.latLng); pano.radius = radius; pano.url = getStreetViewUrl(panorama.data.location.pano) + `&heading=${roadHeading}`; pano.image = `https://streetviewpixels-pa.googleapis.com/v1/thumbnail?w=640&h=360&panoid=${panorama.data.location.pano}&yaw=${roadHeading}&cb_client=maps_sv.share&thumbfov=120`; pano.streetViewEmbed = `https://www.google.com/maps/embed?pb=!4v1749491810223!6m8!1m7!1s${panorama.data.location.pano}!2m2!1d-16.36128053634264!2d-44.39690412269235!3f${roadHeading}!4f0!5f0.4000000000000002`; if (oldRadius && radius >= oldRadius) break; oldRadius = radius; } catch (e) { break; } } return pano; } function removeImage() { const container = document.querySelector( '[class^="guess-map_canvasContainer__"]' ); if (container) { const button = container.querySelector(".guess-preview-button"); if (button) { container.removeChild(button); } } } function getIsClassicGame() { const currentUrl = window.location.href; return ( currentUrl.includes("geoguessr.com/game/") || currentUrl.includes("geoguessr.com/challenge/") ); } function getImage() { if (!getIsClassicGame()) { latestCoords = null; return removeImage(); } const container = document.querySelector( '[class^="guess-map_canvasContainer__"]' ); if (container) { let button = container.querySelector(".guess-preview-button"); if (!button) { button = document.createElement("a"); button.className = "guess-preview-button"; button.target = "_blank"; button.style.zIndex = 10; let img = document.createElement("img"); img.className = "guess-preview"; img.style.width = "100%"; img.style.height = "100%"; img.style.zIndex = 10; img.style.position = "absolute"; img.style.objectFit = "cover"; button.appendChild(img); container.appendChild(button); button.addEventListener("mouseenter", () => { if (SETTINGS.ENLARGE_ON_HOVER && !SETTINGS.EMBED_ON_HOVER) { // Make image higher resolution img.src = img.src.replace("?w=640&h=360", "?w=1024&h=576"); } if ( SETTINGS.EMBED_ON_HOVER && !button.querySelector(".guess-preview-sv-embed") ) { const wrapper = document.createElement("div"); wrapper.className = "guess-preview-sv-embed"; wrapper.style.width = "100%"; wrapper.style.height = "100%"; wrapper.style.position = "absolute"; wrapper.style.zIndex = 11; iframe = document.createElement("iframe"); iframe.style.position = "absolute"; iframe.style.width = "100%"; iframe.style.height = "100%"; iframe.style.border = "0"; iframe.allowFullscreen = true; iframe.loading = "lazy"; iframe.referrerPolicy = "no-referrer-when-downgrade"; iframe.style.zIndex = 12; iframe.src = img.getAttribute("sv-embed"); // img.style.display = "none"; wrapper.appendChild(iframe); button.appendChild(wrapper); } }); } return [button.querySelector(".guess-preview"), button]; } else { return null; } } const originalFetch = unsafeWindow.fetch; let lastRanEpoch = 0; document.addEventListener("keydown", (input) => { const key = input.key; if ( key == KEYBINDINGS.TOGGLE_PREVIEW.toLowerCase() || key == KEYBINDINGS.TOGGLE_PREVIEW ) { isPreviewEnabled = !isPreviewEnabled; GM_setValue("isPreviewEnabled", isPreviewEnabled); if (!isPreviewEnabled) { removeImage(); } else if (latestCoords != null) { setPanoFromCoords(latestCoords); } } }); async function setPanoFromCoords(coords) { if (!isPreviewEnabled) { return removeImage(); } let imgInfo = getImage(); if (!imgInfo || !imgInfo[0] || !imgInfo[1]) { return; } let img = imgInfo[0]; let button = imgInfo[1]; let locationInfo = await getNearestPano(coords); if (!locationInfo || !locationInfo.image) { button.style.display = "none"; return; } button.style.display = "block"; img.style.display = "block"; img.src = locationInfo.image; button.href = locationInfo.url; img.setAttribute("sv-embed", locationInfo.streetViewEmbed); const svEmbed = button.querySelector(".guess-preview-sv-embed"); if (svEmbed) { button.removeChild(svEmbed); } } async function onFetch(args) { if (!getIsClassicGame()) { latestCoords = null; removeImage(); return; } // Cooldown const currentEpoch = Date.now(); const timeBeforeLastEpoch = currentEpoch - lastRanEpoch; lastRanEpoch = currentEpoch; if (timeBeforeLastEpoch < 250) { await new Promise((resolve) => setTimeout(resolve, timeBeforeLastEpoch + 100) ); } if (currentEpoch !== lastRanEpoch) { return; } // Whenever the terrain api gets called, it'll send the coords with it (probably used by Geo to decide to either play the water/plonk SFX) // We'll use that to display the current location if ( args[0] === "https://www.geoguessr.com/api/v4/geo-coding/terrain" && args[1]?.method === "POST" ) { const requestBody = args[1]?.body; if (requestBody) { try { const jsonBody = JSON.parse(requestBody); latestCoords = jsonBody; setPanoFromCoords(jsonBody); } catch (e) { console.error("Failed to parse JSON body:", e); } } } } unsafeWindow.fetch = async function (...args) { Promise.resolve().then(() => onFetch(args)); const response = await originalFetch.apply(this, args); return response; };