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)
});
}));