GeoGuessr Path Logger

Adds a trace of where you have been to GeoGuessr's results screen

  1. // ==UserScript==
  2. // @name GeoGuessr Path Logger
  3. // @version 0.4.3
  4. // @description Adds a trace of where you have been to GeoGuessr's results screen
  5. // @match https://www.geoguessr.com/*
  6. // @author victheturtle#5159
  7. // @license MIT
  8. // @run-at document-start
  9. // @require https://openuserjs.org/src/libs/xsanda/Run_code_as_client.js
  10. // @require https://openuserjs.org/src/libs/xsanda/Google_Maps_Promise.js
  11. // @namespace https://gf.qytechs.cn/users/967692-victheturtle
  12. // ==/UserScript==
  13. // credits to xsanda (https://openuserjs.org/users/xsanda) for the original GeoGuessr Path Logger script
  14.  
  15. /*jshint esversion: 6 */
  16.  
  17. googleMapsPromise.then(() => runAsClient(() => {
  18. const google = window.google;
  19.  
  20. const KEEP_FOR = 1000 * 60 * 60 * 24 * 7 * 4; // 4 weeks
  21.  
  22. // Keep a track of the lines drawn on the map, so they can be removed
  23. let markers = [];
  24.  
  25. const isGamePage = () => location.pathname.includes("/challenge/") || location.pathname.includes("/results/") || location.pathname.includes("/game/") ||
  26. location.pathname.includes("/duels/") || location.pathname.includes("/team-duels/");
  27.  
  28. // Detect if only a single result is shown
  29. const singleResult = () => !!document.querySelector('div[class^="round-result_distanceIndicatorWrapper__"]') ||
  30. (!!document.querySelector('[class^="overlay_backdrop__"], [class^="overlay_overlay__"]') && !document.querySelector('[class^=new-round_roundNumber__]') &&
  31. !document.querySelector('[class^=new-game_container__]')) ||
  32. (!!document.querySelector('div[class^="result-layout_root__"]') && !document.querySelector('div[class^="result-layout_content"]'));
  33.  
  34. // Detect if a results screen is visible, so the traces should be shown
  35. const resultShown = () => singleResult() || !!document.querySelector('div[class^="result-overlay_overlayTotalScore__"]') || location.href.includes('results') ||
  36. !!document.querySelector('[class^="game-summary_container__"]') || !!document.querySelector('div[class^="result-layout_root__"]') || location.href.includes('summary');
  37.  
  38. // Keep a track of whether we are in a round already
  39. let inGame = false;
  40.  
  41. // Get the game ID, for storing the trace against
  42. const id = () => {
  43. const split = location.href.split("/")
  44. if (split[split.length-1] == "summary") return split[split.length-2]
  45. else return split[split.length-1]
  46. }
  47. const roundNumber = () => {
  48. const el = document.querySelector('[data-qa=round-number] :nth-child(2)');
  49. const el2 = document.querySelector('[class^=round-score_roundNumber__]');
  50. return el ? parseInt(el.innerHTML) : (el2 ? parseInt(el2.innerHTML.split(" ")[1]) : 0);
  51. };
  52. const roundID = (n, gameID) => (gameID || id()) + '-' + (n || roundNumber());
  53.  
  54. // Get the location of the street view
  55. const getPosition = sv => ({
  56. lat: sv.position.lat(),
  57. lng: sv.position.lng(),
  58. });
  59.  
  60. // Record the time a game was played
  61. const updateTimestamp = () => {
  62. const timestamps = JSON.parse(localStorage.timestamps || "{}");
  63. timestamps[id()] = Date.now();
  64. localStorage.timestamps = JSON.stringify(timestamps);
  65. };
  66.  
  67. // Remove all games older than a week
  68. const clearOldGames = () => {
  69. const timestamps = JSON.parse(localStorage.timestamps || "{}");
  70. // Delete all games older than a week
  71. const cutoff = Date.now() - KEEP_FOR;
  72. for (const [gameID, gameTime] of Object.entries(timestamps)) {
  73. if (gameTime < cutoff) {
  74. delete timestamps[gameID];
  75. Object.keys(localStorage).filter(key => key.startsWith(gameID)).forEach(key => delete localStorage[key]);
  76. }
  77. }
  78. localStorage.timestamps = JSON.stringify(timestamps);
  79. };
  80.  
  81. const R = 6371.071; // radius of the Earth
  82. const distance = (mk1lat, mk1lng, mk2lat, mk2lng) => {
  83. const rlat1 = mk1lat * (Math.PI / 180)
  84. const rlat2 = mk2lat * (Math.PI / 180)
  85. const difflat = rlat2 - rlat1
  86. const difflon = (mk2lng - mk1lng) * (Math.PI / 180);
  87. const km = 2*R * Math.asin(Math.sqrt(
  88. Math.sin(difflat/2) * Math.sin(difflat/2) +
  89. Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon/2) * Math.sin(difflon/2)
  90. ))
  91. return km;
  92. }
  93.  
  94. clearOldGames();
  95.  
  96. // Keep a track of the current round’s route
  97. let route;
  98.  
  99. let currentRound = undefined;
  100.  
  101. // Keep a track of the start location for the current round, for detecting the return to start button
  102. let start;
  103. let lastPosition = undefined;
  104.  
  105. // Handle the street view being navigated
  106. const onMove = (sv) => {
  107. try {
  108. if (!isGamePage()) return;
  109.  
  110. const position = getPosition(sv);
  111.  
  112. if (!inGame) {
  113. // Do nothing if the map is being updated in the background, e.g. on page load while the results are still shown
  114. if (resultShown()) return;
  115. // otherwise start the round
  116. inGame = true;
  117. start = position;
  118. route = [];
  119. } else if (currentRound !== roundID()) {
  120. currentRound = roundID();
  121. start = position;
  122. route = [];
  123. }
  124.  
  125. // If we’re at the start or moving too far in one click, assume the flag or checkpoint feature were used and begin a new trace
  126. if (position.lat == start.lat && position.lng == start.lng ||
  127. lastPosition != undefined && distance(lastPosition.lat, lastPosition.lng, position.lat, position.lng) > 0.2) {
  128. start = position;
  129. route.push([]);
  130. }
  131.  
  132. lastPosition = position;
  133.  
  134. // Add the location to the trace
  135. route[route.length - 1].push(position);
  136. }
  137. catch (e) {
  138. console.error("GeoGuessr Path Logger Error:", e);
  139. }
  140. };
  141.  
  142. let mapState = 0;
  143.  
  144. // The geometry API isn’t loaded unless a Street View has been displayed since the last load.
  145. const loadGeometry = () => new Promise((resolve, reject) => {
  146. const existingScript = document.querySelector("script[src^='https://maps.googleapis.com/maps-api-v3/api/js/']")
  147. if (!existingScript) reject("No Google Maps loaded yet");
  148. const libraryURL = existingScript.src.replace(/(.+\/)(.+?)(\.js)/, '$1geometry$3');
  149. document.head.appendChild(Object.assign(document.createElement("script"), {
  150. onload: resolve,
  151. type: "text/javascript",
  152. src: libraryURL,
  153. }));
  154. });
  155.  
  156. const onMapUpdate = (map) => {
  157. try {
  158. if (!isGamePage()) return;
  159.  
  160. if (!google.maps.geometry) {
  161. loadGeometry().then(() => onMapUpdate(map));
  162. return;
  163. }
  164.  
  165. // create a checksum of the game state, only updating the map when this changes, to save on computation
  166. const newMapState = (inGame ? 50 : 0) + (resultShown() ? 100 : 0) + (singleResult() ? 200 : 0) + roundNumber();
  167.  
  168. if (newMapState == mapState) return;
  169. mapState = newMapState;
  170.  
  171. // Hide all traces
  172. markers.forEach(m => m.setMap(null));
  173. // If we’re looking at the results, draw the traces again
  174. if (resultShown()) {
  175. // If we were in a round the last time we checked, then we need to save the route
  176. if (inGame) {
  177. // encode the route to reduce the storage required.
  178. const encodedRoutes = route.map(path => google.maps.geometry.encoding.encodePath(path.map(point => new google.maps.LatLng(point))));
  179. localStorage[roundID()] = JSON.stringify(encodedRoutes);
  180. updateTimestamp();
  181. }
  182. inGame = false;
  183. // Show all rounds for the current game when viewing the full results
  184. const roundsToShow = singleResult() ? [roundID()] : Object.keys(localStorage).filter(map => map.startsWith(id()));
  185. markers = roundsToShow
  186. .map(key => localStorage[key]) // Get the map for this round
  187. .filter(r => r) // Ignore missing rounds
  188. .flatMap(r =>
  189. // Render each trace within each round as a red line
  190. JSON.parse(r).map(polyline =>
  191. new google.maps.Polyline({
  192. path: google.maps.geometry.encoding.decodePath(polyline),
  193. geodesic: true,
  194. strokeColor: '#FF0000',
  195. strokeOpacity: 1.0,
  196. strokeWeight: 2,
  197. })
  198. )
  199. );
  200.  
  201. // Add all traces to the map
  202. markers.forEach(m => m.setMap(map));
  203.  
  204. }
  205. }
  206. catch (e) {
  207. console.error("GeoGuessr Path Logger Error:", e);
  208. }
  209. };
  210.  
  211. // When a StreetViewPanorama is constructed, add a listener for moving
  212. const oldSV = google.maps.StreetViewPanorama;
  213. google.maps.StreetViewPanorama = Object.assign(function (...args) {
  214. const res = oldSV.apply(this, args);
  215. this.addListener('position_changed', () => onMove(this));
  216. return res;
  217. }, {
  218. prototype: Object.create(oldSV.prototype)
  219. });
  220.  
  221. // When a Map is constructed, add a listener for updating
  222. const oldMap = google.maps.Map;
  223. google.maps.Map = Object.assign(function (...args) {
  224. const res = oldMap.apply(this, args);
  225. this.addListener('idle', () => onMapUpdate(this));
  226. return res;
  227. }, {
  228. prototype: Object.create(oldMap.prototype)
  229. });
  230. }));

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址