Greasy Fork 支持简体中文。

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.4.9
  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. // @run-at document-start
  10. // @grant GM_addStyle
  11. // @grant GM_info
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_deleteValue
  15. // @grant GM_openInTab
  16. // @grant unsafeWindow
  17. // @grant window.onurlchange
  18. // ==/UserScript==
  19.  
  20. "use strict";
  21.  
  22. const SEARCH_RADIUS = 250000;
  23. const STORAGE_CAP = 30;
  24. const SCRIPT_NAME = GM_info.script.name;
  25. const GAMES_API = "https://www.geoguessr.com/api/v3/games/";
  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, markerListObserver;
  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. await getGuessData(response, options);
  45. }
  46.  
  47. return response;
  48. };
  49. }
  50.  
  51. async function getGuessData(response, options) {
  52. try {
  53. const resp = await response.clone().json();
  54. const token = getGameToken(location.pathname);
  55. const round = resp.round;
  56. const guessList = resp.player.guesses;
  57. const gameFinished = resp.state === "finished";
  58. const challenge = resp.type === "challenge";
  59.  
  60. if (!token) throw new Error("token is null!");
  61.  
  62. if (options.method === "POST") {
  63. const guess = guessList.at(-1);
  64. const coords = { lat: guess.lat, lng: guess.lng };
  65. const pano = await getNearestPano(coords);
  66.  
  67. savePano(token, round, pano);
  68. observeMarker(round);
  69. }
  70.  
  71. if (gameFinished && !challenge) observeMarkerList();
  72. } catch (error) {
  73. console.error(`${SCRIPT_NAME} error: ${error}`);
  74. }
  75. }
  76.  
  77. async function getNearestPano(coords) {
  78. let pano = {};
  79. let panorama, oldRadius;
  80. let radius = SEARCH_RADIUS;
  81. if (!svs) initSVS();
  82.  
  83. // eslint-disable-next-line no-constant-condition
  84. while (true) {
  85. try {
  86. panorama = await svs.getPanorama({
  87. location: coords,
  88. radius: radius,
  89. source: "outdoor",
  90. preference: "nearest",
  91. });
  92.  
  93. radius = computeDistanceBetween(coords, panorama.data.location.latLng);
  94. pano.radius = radius;
  95. pano.url = getStreetViewUrl(panorama.data.location.pano);
  96.  
  97. if (oldRadius && radius >= oldRadius) break;
  98. oldRadius = radius;
  99. } catch (e) {
  100. break;
  101. }
  102. }
  103.  
  104. return pano;
  105. }
  106.  
  107. function savePano(token, round, pano) {
  108. const panos = GM_getValue(token, { [round]: pano });
  109. const history = GM_getValue("history", []);
  110.  
  111. panos[round] = pano;
  112. GM_setValue(token, panos);
  113.  
  114. // Don't duplicate the same token for each pano.
  115. if (!history.includes(token)) {
  116. history.unshift(token);
  117. GM_setValue("history", history);
  118. }
  119.  
  120. cleanStorage(history);
  121. }
  122.  
  123. function cleanStorage(history) {
  124. if (history.length > STORAGE_CAP) {
  125. const lastItem = history.pop();
  126.  
  127. GM_setValue("history", history);
  128. GM_deleteValue(lastItem);
  129. }
  130. }
  131.  
  132. function observeMarker(round) {
  133. markerObserver = new MutationObserver(() => {
  134. const roundEnd = document.querySelector(SELECTORS.roundEnd);
  135. const marker = document.querySelector(SELECTORS.marker);
  136. const token = getGameToken(location.pathname);
  137. const panoList = GM_getValue(token);
  138.  
  139. if (!(roundEnd && marker && panoList)) return;
  140.  
  141. markerObserver.disconnect();
  142. updateMarker(marker, panoList[round]);
  143. });
  144.  
  145. markerObserver.observe(document.body, {
  146. childList: true,
  147. subtree: true,
  148. });
  149. }
  150.  
  151. function observeMarkerList() {
  152. markerListObserver = new MutationObserver(() => {
  153. const results =
  154. document.querySelector(SELECTORS.results) ||
  155. document.querySelector(SELECTORS.gameEnd);
  156. const markerList = document.querySelectorAll(SELECTORS.markerList);
  157. const token = getGameToken(location.pathname);
  158. const panoList = GM_getValue(token);
  159.  
  160. // No point in checking this page.
  161. if (!panoList) return markerListObserver.disconnect();
  162. if (!(results && markerList.length > 0)) return;
  163.  
  164. markerListObserver.disconnect();
  165. for (const [round, pano] of Object.entries(panoList)) {
  166. const marker = markerList.item(parseInt(round) - 1);
  167. updateMarker(marker, pano);
  168. }
  169. });
  170.  
  171. markerListObserver.observe(document.body, {
  172. childList: true,
  173. subtree: true,
  174. });
  175. }
  176.  
  177. function updateMarker(marker, pano) {
  178. let distance = convertDistance(SEARCH_RADIUS);
  179. const tooltip = document.createElement("div");
  180. tooltip.className = "peek-tooltip";
  181. tooltip.textContent = `No location was found within ${distance}!`;
  182.  
  183. marker.setAttribute("data-pano", "false");
  184.  
  185. if (Object.keys(pano).length > 0) {
  186. distance = convertDistance(pano.radius);
  187. tooltip.textContent = `Click to see the nearest location! [${distance}]`;
  188.  
  189. marker.setAttribute("data-pano", "true");
  190. marker.addEventListener("click", () => {
  191. GM_openInTab(pano.url, { active: true });
  192. });
  193. }
  194.  
  195. marker.append(tooltip);
  196. }
  197.  
  198. function initObservers() {
  199. stopObservers();
  200. if (inResults()) observeMarkerList();
  201. }
  202.  
  203. function stopObservers() {
  204. if (markerObserver) markerObserver.disconnect();
  205. if (markerListObserver) markerListObserver.disconnect();
  206. }
  207.  
  208. function inResults() {
  209. return location.pathname.includes("/results/");
  210. }
  211.  
  212. function getGameToken(url) {
  213. const token = url.match(/[0-9a-zA-Z]{16}/);
  214. return token ? token[0] : null;
  215. }
  216.  
  217. function getStreetViewUrl(panoId) {
  218. return `https://www.google.com/maps/@?api=1&map_action=pano&pano=${panoId}`;
  219. }
  220.  
  221. function convertDistance(distance) {
  222. if (distance >= 1000) return (distance / 1000).toFixed(1) + " km";
  223. return distance.toFixed(1) + " m";
  224. }
  225.  
  226. function computeDistanceBetween(coords1, coords2) {
  227. return unsafeWindow.google.maps.geometry.spherical.computeDistanceBetween(
  228. coords1,
  229. coords2,
  230. );
  231. }
  232.  
  233. function initSVS() {
  234. svs = new unsafeWindow.google.maps.StreetViewService();
  235. }
  236.  
  237. function main() {
  238. interceptFetch();
  239. initObservers();
  240. window.addEventListener("urlchange", initObservers);
  241.  
  242. console.log(`${SCRIPT_NAME} is running!`);
  243.  
  244. GM_addStyle(`
  245. .peek-tooltip {
  246. display: none;
  247. position: absolute;
  248. width: 120px;
  249. background: #323232;
  250. border-radius: 4px;
  251. text-align: center;
  252. padding: 0.5rem;
  253. font-size: 0.9rem;
  254. right: 50%;
  255. bottom: 220%;
  256. margin-right: -60px;
  257. opacity: 90%;
  258. z-index: 4;
  259. }
  260.  
  261. .peek-tooltip:after {
  262. content: "";
  263. position: absolute;
  264. top: 100%;
  265. left: 50%;
  266. margin-left: -5px;
  267. border-width: 5px;
  268. border-style: solid;
  269. border-color: #323232 transparent transparent transparent;
  270. }
  271.  
  272. [data-pano="true"]:hover .peek-tooltip,
  273. [data-pano="false"]:hover .peek-tooltip {
  274. display: block;
  275. }
  276.  
  277. [data-pano="true"] > :first-child {
  278. cursor: pointer;
  279. --border-color: #E91E63 !important;
  280. --border-size-factor: 2 !important;
  281. }
  282.  
  283. [data-pano="false"] > :first-child {
  284. cursor: initial;
  285. --border-color: #323232 !important;
  286. --border-size-factor: 1.5 !important;
  287. }
  288. `);
  289. }
  290.  
  291. main();