GeoHintr

GeoHintr - Allows you to place written hints on your maps

  1. // ==UserScript==
  2. // @name GeoHintr
  3. // @namespace MrMike/GeoGuessr/GeoHintr
  4. // @description GeoHintr - Allows you to place written hints on your maps
  5. // @author MrMike & Alok
  6. // @version 0.4
  7. // @include /^(https?)?(\:)?(\/\/)?([^\/]*\.)?geoguessr\.com($|\/.*)/
  8. // @grant none
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. const GH = {
  13. MYHINTS: [],
  14. round: 0,
  15. hintDiv: null,
  16. uiDiv: null,
  17. bigImage: false,
  18. tokens: [],
  19. setHints: (hints) => {
  20. GH.MYHINTS = hints;
  21. },
  22. init: () => {
  23. const targetNode = document.getElementsByTagName("body")[0];
  24. const config = {
  25. attributes: false,
  26. childList: true,
  27. subtree: false,
  28. characterData: false
  29. };
  30. const observer = new MutationObserver((mutationsList, observer) => {
  31. for (const mutation of mutationsList) {
  32. if (mutation.type === "childList") {
  33. GH.checkRound();
  34. }
  35. }
  36. });
  37. observer.observe(targetNode, config);
  38. if (!GH.hintDiv) {
  39. GH.hintDiv = document.createElement("div");
  40. GH.hintDiv.setAttribute("class", "game-statuses");
  41. GH.hintDiv.setAttribute("id", "GH-hint");
  42. GH.hintDiv.setAttribute("style", "display: inline-flex; padding: 5px 0.5rem 0px; margin-top: 2px;");
  43. GH.hintDiv.style.display = "none";
  44. }
  45. GH.handleStorage();
  46. GH.createUI();
  47. GH.keyListener();
  48. },
  49. handleStorage: () => {
  50. GH.tokens = JSON.parse(sessionStorage.getItem("GH-TOKENS"));
  51. if (GH.tokens && GH.tokens.length > 0) {
  52. }
  53. else {
  54. sessionStorage.setItem("GH-TOKENS", JSON.stringify([]));
  55. GH.tokens = JSON.parse(sessionStorage.getItem("GH-TOKENS"));
  56. }
  57. },
  58. storeTokens: () => {
  59. sessionStorage.setItem("GH-TOKENS", JSON.stringify(GH.tokens));
  60. },
  61. removeToken: (token) => {
  62. for (let i = 0; i < GH.tokens.length; i++) {
  63. if (GH.tokens[i].token === token) {
  64. GH.tokens.splice(i, 1);
  65. }
  66. }
  67. GH.storeTokens();
  68. GH.redrawUI();
  69. },
  70. checkRound: () => {
  71. const roundData = document.querySelector("div[data-qa='round-number']");
  72. if (roundData) {
  73. let roundElement = roundData.querySelector("[class^='status_value']");
  74. if (roundElement) {
  75. let round = parseInt(roundElement.innerText.charAt(0));
  76. if (!isNaN(round) && round >= 1 && round <= 5) {
  77. if (round != GH.round) {
  78. GH.round = round;
  79. GH.doMagic();
  80. }
  81. }
  82. }
  83. }
  84. },
  85. doMagic: () => {
  86. let URL = null;
  87. if (window.location.pathname.includes("game")) {
  88. URL = `https://www.geoguessr.com/api/v3/games/${window.location.pathname.substring(6)}`;
  89. }
  90. else if (window.location.pathname.includes("challenge")) {
  91. URL = `https://www.geoguessr.com/api/v3/challenges/${window.location.pathname.substring(11)}/game`;
  92. }
  93. if (URL) {
  94. fetch(URL)
  95. .then((response) => response.json())
  96. .then((data) => {
  97. const { lat, lng } = data.rounds[data.round - 1];
  98. let coordinates = { lat, lng };
  99. GH.checkForHints(coordinates);
  100. })
  101. .catch((error) => {
  102. console.log("Something went wrong");
  103. console.log(error);
  104. });
  105. }
  106. },
  107. checkForHints: (coordinates) => {
  108. let removeHint = true;
  109. GH.MYHINTS.forEach(hint => {
  110. if (GH.coordinatesInRange(coordinates, { lat: hint.lat, lng: hint.lng })) {
  111. GH.updateHint(hint);
  112. removeHint = false;
  113. }
  114. });
  115. if (removeHint) {
  116. GH.hintDiv.style.display = "none";
  117. }
  118. },
  119. handleSpoilers: (hint) => {
  120.  
  121. return "";
  122. },
  123. updateHint: (hint) => {
  124. GH.hintDiv.innerHTML = `
  125. <div class="game-status__body">
  126. <style>
  127. #wrapper {
  128. text-align: center;
  129. background-color: #FFFFFF66;
  130. color: black;
  131. border-radius: 12px;
  132. padding: 12px;
  133. font-size: 1.2rem;
  134. max-width: 0.5vw;
  135. max-height: 0vh;
  136. padding-top: 24px;
  137. position: relative;
  138. float: right;
  139. overflow: hidden;
  140. transition: all 0.5s ease-in-out;
  141. }
  142. #shrink {
  143. position: absolute;
  144. right: 4px;
  145. top: -10px;
  146. cursor: pointer;
  147. font-size: 2.5rem;
  148. transition: all 0.5s ease-in-out;
  149. }
  150. #hint{
  151. margin-top: 12px;
  152. transition: all 0.5s ease-in-out;
  153. }
  154. </style>
  155. <div id="wrapper">
  156. <div id="shrink" title="Show hint">&#164;</div>
  157. <p id="hint"></p>
  158. <div class="spacer" style="clear: both;"></div>
  159. </div>
  160. </div>
  161. `;
  162.  
  163. if (document.getElementById("GH-hint")) {
  164. GH.hintDiv.style.display = "";
  165. }
  166. else {
  167. document.querySelector(".game-layout__status").appendChild(GH.hintDiv);
  168. GH.hintDiv.style.display = "";
  169. }
  170.  
  171. document.getElementById("hint").innerText = hint.hint;
  172.  
  173. let wrapper = document.getElementById("wrapper");
  174. let shrink = document.getElementById("shrink");
  175. shrink.addEventListener("click", () => {
  176. if (shrink.title == "Hide") {
  177. shrink.setAttribute("title", "Show hint");
  178. shrink.style.fontSize = "2.5rem";
  179. shrink.style.top = "-10px";
  180. shrink.style.right = "4px";
  181. wrapper.style.maxWidth = "0.5vw";
  182. wrapper.style.maxHeight = "0vh";
  183. wrapper.style.paddingTop = "20px";
  184. //document.getElementById("hint").style.marginTop = "12px";
  185. }
  186. else {
  187. shrink.setAttribute("title", "Hide");
  188. shrink.style.fontSize = "1.75rem";
  189. shrink.style.top = "0px";
  190. shrink.style.right = "8px";
  191. wrapper.style.maxWidth = "30vw";
  192. wrapper.style.maxHeight = "75vh";
  193. wrapper.style.paddingTop = "12px";
  194. //document.getElementById("hint").style.marginTop = "0px";
  195. }
  196. });
  197.  
  198. if (hint.img) {
  199. let image = document.createElement("img");
  200. image.setAttribute("title", "Click to enlarge");
  201. image.src = hint.img;
  202. image.style.maxHeight = "25vh";
  203. image.style.cursor = "pointer";
  204. image.style.transition = "all 0.5s ease-in-out";
  205. let wrapper = document.getElementById("wrapper");
  206. image.addEventListener("click", () => {
  207. if (shrink.title == "Hide") {
  208. if (GH.bigImage) {
  209. image.style.maxHeight = "25vh";
  210. wrapper.style.backgroundColor = "#FFFFFF66";
  211. wrapper.style.maxWidth = "30vw";
  212. }
  213. else {
  214. image.style.maxHeight = "75vh";
  215. wrapper.style.backgroundColor = "#FFFFFFCC";
  216. wrapper.style.maxWidth = "50vw";
  217. }
  218. GH.bigImage = !GH.bigImage;
  219. }
  220. });
  221. wrapper.appendChild(image);
  222. }
  223. if (hint.video) {
  224. let iframe = document.createElement("iframe");
  225. iframe.src = `https://www.youtube.com/embed/${hint.video}`;
  226. iframe.width = "100%";
  227. iframe.style.width = "25vw";
  228. iframe.style.height = "14.0625vw";
  229. iframe.setAttribute("allowfullscreen", "");
  230. iframe.setAttribute("frameborder", "0");
  231. wrapper.appendChild(iframe);
  232. }
  233. },
  234. loadHints: (token, name) => {
  235. document.getElementById("GH-message").innerText = "Loading...";
  236. fetch(`https://dongles.vercel.app/dongle/paste`, {
  237. method: "POST",
  238. headers: {
  239. "Content-Type": "application/json"
  240. },
  241. body: JSON.stringify({ token: token })
  242. })
  243. .then((results) => {
  244. return results.json();
  245. })
  246. .then((data) => {
  247. if (data.error) {
  248. console.log(data);
  249. throw new Error(data.error.message);
  250. }
  251. GH.setHints(data);
  252. document.getElementById("GH-message").innerText = "Hints loaded...";
  253. GH.doMagic();
  254. setTimeout(() => {
  255. GH.hideUI();
  256. }, 2000);
  257. })
  258. .catch((error) => {
  259. console.log(error);
  260. document.getElementById("GH-message").innerText = "Something went wrong...";
  261. });
  262. let exists = false;
  263. GH.tokens.forEach((tok) => {
  264. if (tok.token === token) {
  265. exists = true;
  266. }
  267. });
  268. if (!exists) {
  269. GH.tokens.push({ token, name });
  270. }
  271. GH.storeTokens();
  272. GH.redrawUI();
  273. },
  274. coordinatesInRange: (original, hint) => {
  275. let ky = 40000 / 360;
  276. let kx = Math.cos(Math.PI * hint.lat / 180.0) * ky;
  277. let dx = Math.abs(hint.lng - original.lng) * kx;
  278. let dy = Math.abs(hint.lat - original.lat) * ky;
  279. return Math.sqrt(dx * dx + dy * dy) <= 0.050;
  280. },
  281. keyListener: () => {
  282. document.addEventListener("keydown", (event) => {
  283. if (event.code === "KeyH" && event.ctrlKey && event.altKey && !event.shiftKey && !event.metaKey && !event.repeat) {
  284. if (GH.uiDiv.style.display === "block") {
  285. GH.hideUI();
  286. }
  287. else {
  288. GH.showUI();
  289. }
  290. }
  291. });
  292. },
  293. createUI: () => {
  294. if (!GH.uiDiv) {
  295. GH.uiDiv = document.createElement("div");
  296. GH.uiDiv.setAttribute("id", "GH-ui")
  297.  
  298. Object.assign(GH.uiDiv.style, {
  299. display: "none",
  300. position: "fixed",
  301. backgroundColor: "#eee9e0",
  302. zIndex: "1000",
  303. width: "fit-content",
  304. height: "fit-content",
  305. top: "48px",
  306. left: "8px",
  307. padding: "20px",
  308. borderRadius: "10px",
  309. boxShadow: "0 2px 2px 0",
  310. overflow: "hidden"
  311. });
  312.  
  313. GH.uiDiv.innerHTML = ``;
  314. GH.tokens.forEach((token) => {
  315. GH.uiDiv.innerHTML += `
  316. <input type="text" size="10" id="${token.token}" value="${token.token}" />
  317. <input type="text" value="${token.name}" />
  318. <button id="GH-${token.token}">LOAD</button>
  319. <button id="GHR-${token.token}">X</button>
  320. <br />
  321. `;
  322. });
  323. GH.uiDiv.innerHTML += `
  324. <input type="text" placeholder="Pastebin Token" size="10" id="pastebin_token" />
  325. <input type="text" placeholder="Description" id="pastebin_name" />
  326. <button id="GH-load">LOAD</button>
  327. <br />
  328. `;
  329. GH.uiDiv.innerHTML += `
  330. <div style="text-align: center; margin-top: 8px">
  331. <p id="GH-message"></p>
  332. </div>
  333. `;
  334. }
  335. document.body.appendChild(GH.uiDiv);
  336. document.getElementById("GH-load").addEventListener("click", () => {
  337. GH.loadHints(document.getElementById("pastebin_token").value, document.getElementById("pastebin_name").value);
  338. });
  339. GH.tokens.forEach((token) => {
  340. document.getElementById(`GH-${token.token}`).addEventListener("click", () => GH.loadHints(token.token, token.name));
  341. });
  342. GH.tokens.forEach((token) => {
  343. document.getElementById(`GHR-${token.token}`).addEventListener("click", () => GH.removeToken(token.token));
  344. });
  345. },
  346. redrawUI: () => {
  347. GH.hideUI();
  348. document.getElementById("GH-ui").remove();
  349. GH.uiDiv = null;
  350. GH.createUI();
  351. GH.showUI();
  352. },
  353. showUI: () => {
  354. GH.uiDiv.style.display = "block";
  355. },
  356. hideUI: () => {
  357. GH.uiDiv.style.display = "none";
  358. }
  359. }
  360.  
  361. GH.init();