Guess Peek (Geoguessr)

Click on your pin to see where you've guessed!

  1. // ==UserScript==
  2. // @name Guess Peek (Geoguessr)
  3. // @namespace alienperfect
  4. // @version 1.5.1
  5. // @description Click on your pin to see where you've guessed!
  6. // @author Alien Perfect
  7. // @match https://www.geoguessr.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=32&domain=geoguessr.com
  9. // @grant GM_addStyle
  10. // @grant GM_info
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_deleteValue
  14. // @grant GM_openInTab
  15. // @grant unsafeWindow
  16. // @grant window.onurlchange
  17. // ==/UserScript==
  18.  
  19. "use strict";
  20.  
  21. const SEARCH_RADIUS = 250000;
  22. const STORAGE_CAP = 30;
  23. const SCRIPT_NAME = GM_info.script.name;
  24. const GAMES_API = "https://www.geoguessr.com/api/v3/games/";
  25.  
  26. const SELECTORS = {
  27. marker: "[data-qa='guess-marker']",
  28. markerList: "[class*='map-pin_']:not([data-qa='correct-location-marker'])",
  29. roundEnd: "[data-qa='close-round-result']",
  30. gameEnd: "[data-qa='play-again-button']",
  31. results: "[data-qa='results-map']",
  32. };
  33.  
  34. let svs, markerObserver;
  35.  
  36. function interceptFetch() {
  37. const _fetch = unsafeWindow.fetch;
  38.  
  39. unsafeWindow.fetch = async (resource, options) => {
  40. const response = await _fetch(resource, options);
  41. const url = resource.toString();
  42.  
  43. if (url.includes(GAMES_API) && options) {
  44. queueMicrotask(async () => {
  45. const resp = await response.clone().json();
  46. await getGuessData(resp, options);
  47. });
  48. }
  49.  
  50. return response;
  51. };
  52. }
  53.  
  54. async function getGuessData(resp, options) {
  55. try {
  56. const token = resp.token;
  57. const round = resp.round;
  58. const guesses = resp.player.guesses;
  59. const isGameFinished = resp.state === "finished";
  60. const isChallenge = resp.type === "challenge";
  61.  
  62. if (options.method === "POST") {
  63. const guess = guesses.at(-1);
  64. const pano = await getNearestPano({ lat: guess.lat, lng: guess.lng });
  65.  
  66. savePano(token, round, pano);
  67. updateMarker(getMarker(round), pano);
  68. }
  69.  
  70. if (isGameFinished && !isChallenge) observeMarkers();
  71. } catch (e) {
  72. console.error(`${SCRIPT_NAME} error: ${e}`);
  73. }
  74. }
  75.  
  76. async function getNearestPano(coords) {
  77. const nearestPano = {};
  78. let radius = SEARCH_RADIUS;
  79. let oldRadius;
  80. if (!svs) initSVS();
  81.  
  82. for (;;) {
  83. try {
  84. const pano = await svs.getPanorama({
  85. location: coords,
  86. radius: radius,
  87. sources: ["outdoor"],
  88. preference: "nearest",
  89. });
  90.  
  91. radius = getRadius(coords, pano.data.location.latLng);
  92. if (oldRadius && radius >= oldRadius) break;
  93.  
  94. nearestPano.radius = radius;
  95. nearestPano.url = getStreetViewUrl(pano.data.location.pano);
  96. oldRadius = radius;
  97. } catch (e) {
  98. console.error(`${SCRIPT_NAME} error: ${e}`);
  99. break;
  100. }
  101. }
  102.  
  103. return nearestPano;
  104. }
  105.  
  106. function savePano(token, round, pano) {
  107. const panos = GM_getValue(token, { [round]: pano });
  108. const history = GM_getValue("history", []);
  109.  
  110. panos[round] = pano;
  111. GM_setValue(token, panos);
  112.  
  113. if (!history.includes(token)) {
  114. history.unshift(token);
  115. GM_setValue("history", history);
  116. }
  117.  
  118. cleanStorage(history);
  119. }
  120.  
  121. function cleanStorage(history) {
  122. if (history.length > STORAGE_CAP) {
  123. const lastItem = history.pop();
  124.  
  125. GM_setValue("history", history);
  126. GM_deleteValue(lastItem);
  127. }
  128. }
  129.  
  130. function getMarker() {
  131. const marker = document.querySelector(SELECTORS.marker);
  132. const roundEnd = document.querySelector(SELECTORS.roundEnd);
  133.  
  134. if (marker && roundEnd) return marker;
  135. }
  136.  
  137. function observeMarkers() {
  138. markerObserver = new MutationObserver(() => {
  139. const token = getGameToken(location.pathname); // can break on start without try-catch
  140. const panoList = GM_getValue(token);
  141. if (!panoList) return stopObserver();
  142.  
  143. const results =
  144. document.querySelector(SELECTORS.results) ||
  145. document.querySelector(SELECTORS.gameEnd);
  146. const markerList = document.querySelectorAll(SELECTORS.markerList);
  147.  
  148. if (!(results && markerList.length > 0)) return;
  149. stopObserver();
  150.  
  151. for (const [round, pano] of Object.entries(panoList)) {
  152. const marker = markerList.item(parseInt(round) - 1);
  153. updateMarker(marker, pano);
  154. }
  155. });
  156.  
  157. markerObserver.observe(document.body, {
  158. childList: true,
  159. subtree: true,
  160. });
  161. }
  162.  
  163. function updateMarker(marker, pano) {
  164. let distance = formatDistance(SEARCH_RADIUS);
  165. const tooltip = document.createElement("div");
  166. tooltip.className = "peek-tooltip";
  167. tooltip.textContent = `No location was found within ${distance}!`;
  168.  
  169. marker.setAttribute("data-pano", "false");
  170.  
  171. if (Object.keys(pano).length > 0) {
  172. distance = formatDistance(pano.radius);
  173. tooltip.textContent = `Click to see the nearest location! [${distance}]`;
  174.  
  175. marker.setAttribute("data-pano", "true");
  176. marker.addEventListener("click", () => {
  177. GM_openInTab(pano.url, { active: true });
  178. });
  179. }
  180.  
  181. marker.append(tooltip);
  182. }
  183.  
  184. function startObserver() {
  185. stopObserver();
  186. if (inResults()) observeMarkers();
  187. }
  188.  
  189. function stopObserver() {
  190. if (markerObserver) markerObserver.disconnect();
  191. }
  192.  
  193. function inResults() {
  194. return location.pathname.includes("/results/");
  195. }
  196.  
  197. function getGameToken(url) {
  198. const token = url.match(/[0-9a-zA-Z]{16}/);
  199. return token ? token[0] : null;
  200. }
  201.  
  202. function getStreetViewUrl(panoId) {
  203. return `https://www.google.com/maps/@?api=1&map_action=pano&pano=${panoId}`;
  204. }
  205.  
  206. function formatDistance(num) {
  207. let units = "m";
  208.  
  209. if (num >= 1000) {
  210. num = num / 1000;
  211. units = "km";
  212. }
  213.  
  214. return Math.floor(num) + " " + units;
  215. }
  216.  
  217. function getRadius(coords1, coords2) {
  218. return unsafeWindow.google.maps.geometry.spherical.computeDistanceBetween(
  219. coords1,
  220. coords2,
  221. );
  222. }
  223.  
  224. function initSVS() {
  225. svs = new unsafeWindow.google.maps.StreetViewService();
  226. }
  227.  
  228. function main() {
  229. interceptFetch();
  230. startObserver();
  231. window.addEventListener("urlchange", startObserver);
  232.  
  233. console.log(`${SCRIPT_NAME} is doing things!`);
  234.  
  235. GM_addStyle(`
  236. .peek-tooltip {
  237. display: none;
  238. position: absolute;
  239. width: 120px;
  240. background: #323232;
  241. border-radius: 4px;
  242. text-align: center;
  243. padding: 0.5rem;
  244. font-size: 0.9rem;
  245. right: 50%;
  246. bottom: 220%;
  247. margin-right: -60px;
  248. opacity: 90%;
  249. z-index: 4;
  250. }
  251.  
  252. .peek-tooltip:after {
  253. content: "";
  254. position: absolute;
  255. top: 100%;
  256. left: 50%;
  257. margin-left: -5px;
  258. border-width: 5px;
  259. border-style: solid;
  260. border-color: #323232 transparent transparent transparent;
  261. }
  262.  
  263. [data-pano="true"]:hover .peek-tooltip,
  264. [data-pano="false"]:hover .peek-tooltip {
  265. display: block;
  266. }
  267.  
  268. [data-pano="true"] > :first-child {
  269. cursor: pointer;
  270. --border-color: #E91E63 !important;
  271. --border-size-factor: 2 !important;
  272. }
  273.  
  274. [data-pano="false"] > :first-child {
  275. cursor: initial;
  276. --border-color: #323232 !important;
  277. --border-size-factor: 1.5 !important;
  278. }
  279. `);
  280. }
  281.  
  282. main();