NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name GeoGuessr Path Logger // @namespace xsanda // @description Add a trace of where you have been to GeoGuessr’s results screen // @version 0.3.5 // @include https://www.geoguessr.com/* // @require https://openuserjs.org/src/libs/xsanda/Run_code_as_client.js // @require https://openuserjs.org/src/libs/xsanda/Google_Maps_Promise.js // @updateURL https://openuserjs.org/meta/xsanda/GeoGuessr_Path_Logger.meta.js // @downloadURL https://openuserjs.org/install/xsanda/GeoGuessr_Path_Logger.user.js // @copyright 2021, xsanda (https://openuserjs.org/users/xsanda) // @run-at document-start // @license MIT // ==/UserScript== /*jshint esversion: 6 */ googleMapsPromise.then(() => runAsClient(() => { const google = window.google; const KEEP_FOR = 1000 * 60 * 60 * 24 * 7; // 1 week // Keep a track of the lines drawn on the map, so they can be removed let markers = []; const isGamePage = () => location.pathname.startsWith("/challenge/") || location.pathname.startsWith("/results/") || location.pathname.startsWith("/game/"); // Detect if a results screen is visible, so the traces should be shown const resultShown = () => !!document.querySelector('[data-qa=result-view-bottom]') || location.href.includes('results'); // Detect if only a single result is shown const singleResult = () => !!document.querySelector("[data-qa=guess-description]") || !!document.querySelector('.country-streak-result__sub-title'); // Keep a track of whether we are in a round already let inGame = false; // Get the game ID, for storing the trace against const id = () => location.href.match(/\w{15,}/); const roundNumber = () => { const el = document.querySelector('[data-qa=round-number] :nth-child(2)'); return el ? parseInt(el.innerHTML) : 0; }; const roundID = (n, gameID) => (gameID || id()) + '-' + (n || roundNumber()); // Get the location of the street view const getPosition = sv => ({ lat: sv.position.lat(), lng: sv.position.lng(), }); // Record the time a game was played const updateTimestamp = () => { const timestamps = JSON.parse(localStorage.timestamps || "{}"); timestamps[id()] = Date.now(); localStorage.timestamps = JSON.stringify(timestamps); }; // Remove all games older than a week const clearOldGames = () => { const timestamps = JSON.parse(localStorage.timestamps || "{}"); // Delete all games older than a week const cutoff = Date.now() - KEEP_FOR; for (const [gameID, gameTime] of Object.entries(timestamps)) { if (gameTime < cutoff) { delete timestamps[gameID]; for (let i = 1; i <= 5; i++) delete localStorage[roundID(i, gameID)]; } } localStorage.timestamps = JSON.stringify(timestamps); }; clearOldGames(); // Keep a track of the current round’s route let route; let currentRound = undefined; // Keep a track of the start location for the current round, for detecting the return to start button let start; // Handle the street view being navigated const onMove = (sv) => { try { if (!isGamePage()) return; const position = getPosition(sv); if (!inGame) { // Do nothing if the map is being updated in the background, e.g. on page load while the results are still shown if (resultShown()) return; // otherwise start the round inGame = true; start = position; route = []; } else if (currentRound !== roundID()) { currentRound = roundID(); start = position; route = []; } // If we’re at the start, begin a new trace if (position.lat == start.lat && position.lng == start.lng) route.push([]); // Add the location to the trace route[route.length - 1].push(position); } catch (e) { console.error("GeoGuessr Path Logger Error:", e); } }; let mapState = 0; // The geometry API isn’t loaded unless a Street View has been displayed since the last load. const loadGeometry = () => new Promise((resolve, reject) => { const existingScript = document.querySelector("script[src^='https://maps.googleapis.com/maps-api-v3/api/js/']") if (!existingScript) reject("No Google Maps loaded yet"); const libraryURL = existingScript.src.replace(/(.+\/)(.+?)(\.js)/, '$1geometry$3'); document.head.appendChild(Object.assign(document.createElement("script"), { onload: resolve, type: "text/javascript", src: libraryURL, })); }); const onMapUpdate = (map) => { try { if (!isGamePage()) return; if (!google.maps.geometry) { loadGeometry().then(() => onMapUpdate(map)); return; } // create a checksum of the game state, only updating the map when this changes, to save on computation const newMapState = (inGame ? 5 : 0) + (resultShown() ? 10 : 0) + (singleResult() ? 20 : 0) + roundNumber(); if (newMapState == mapState) return; mapState = newMapState; // Hide all traces markers.forEach(m => m.setMap(null)); // If we’re looking at the results, draw the traces again if (resultShown()) { // If we were in a round the last time we checked, then we need to save the route if (inGame) { // encode the route to reduce the storage required. const encodedRoutes = route.map(path => google.maps.geometry.encoding.encodePath(path.map(point => new google.maps.LatLng(point)))); localStorage[roundID()] = JSON.stringify(encodedRoutes); updateTimestamp(); } inGame = false; // Show all rounds for the current game when viewing the full results const roundsToShow = singleResult() ? [roundID()] : Object.keys(localStorage).filter(map => map.startsWith(id())); markers = roundsToShow .map(key => localStorage[key]) // Get the map for this round .filter(r => r) // Ignore missing rounds .flatMap(r => // Render each trace within each round as a red line JSON.parse(r).map(polyline => new google.maps.Polyline({ path: google.maps.geometry.encoding.decodePath(polyline), geodesic: true, strokeColor: '#FF0000', strokeOpacity: 1.0, strokeWeight: 2, }) ) ); // Add all traces to the map markers.forEach(m => m.setMap(map)); } } catch (e) { console.error("GeoGuessr Path Logger Error:", e); } }; // When a StreetViewPanorama is constructed, add a listener for moving const oldSV = google.maps.StreetViewPanorama; google.maps.StreetViewPanorama = Object.assign(function (...args) { const res = oldSV.apply(this, args); this.addListener('position_changed', () => onMove(this)); return res; }, { prototype: Object.create(oldSV.prototype) }); // When a Map is constructed, add a listener for updating const oldMap = google.maps.Map; google.maps.Map = Object.assign(function (...args) { const res = oldMap.apply(this, args); this.addListener('idle', () => onMapUpdate(this)); return res; }, { prototype: Object.create(oldMap.prototype) }); }));