Geoguessr Map-Making Auto-Tag

Tag your street views by date&address&generations

当前为 2024-05-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Geoguessr Map-Making Auto-Tag
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.5
  5. // @description Tag your street views by date&address&generations
  6. // @author KaKa
  7. // @match https://map-making.app/maps/*
  8. // @grant GM_setClipboard
  9. // @grant GM_xmlhttpRequest
  10. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  11. // @license MIT
  12. // @icon https://www.google.com/s2/favicons?domain=geoguessr.com
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. let tagBox = ['year', 'month', 'country', 'subdivision', 'locality', 'generations', 'type', 'coverageCount']
  19.  
  20. async function runScript(tags,sR) {
  21. const { value: option,dismiss: inputDismiss } = await Swal.fire({
  22. title: 'Input JSON Data',
  23. text: 'Do you want to input data from the clipboard? If you click "Cancel", you will need to upload a JSON file.',
  24. icon: 'question',
  25. showCancelButton: true,
  26. showCloseButton:true,
  27. allowOutsideClick: false,
  28. confirmButtonColor: '#3085d6',
  29. cancelButtonColor: '#d33',
  30. confirmButtonText: 'Yes',
  31. cancelButtonText: 'Cancel'
  32. });
  33.  
  34. let data;
  35. if (option) {
  36.  
  37. const text = await navigator.clipboard.readText();
  38. try {
  39. data = JSON.parse(text);
  40. } catch (error) {
  41. Swal.fire('Error parsing JSON data! ', 'The input JSON data is invalid or incorrectly formatted.','error');
  42. return;
  43. }
  44. } else if(inputDismiss==='cancel'){
  45.  
  46. const input = document.createElement('input');
  47. input.type = 'file';
  48. input.style.display = 'none'
  49. document.body.appendChild(input);
  50.  
  51. data = await new Promise((resolve) => {
  52. input.addEventListener('change', async () => {
  53. const file = input.files[0];
  54. const reader = new FileReader();
  55.  
  56. reader.onload = (event) => {
  57. try {
  58. const result = JSON.parse(event.target.result);
  59. resolve(result);
  60.  
  61. document.body.removeChild(input);
  62. } catch (error) {
  63. Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invalid or incorrectly formatted.','error');
  64. }
  65. };
  66.  
  67. reader.readAsText(file);
  68. });
  69.  
  70.  
  71. input.click();
  72. });
  73. }
  74. const newData = [];
  75.  
  76.  
  77. async function UE(t, e) {
  78. try {
  79. const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
  80. let payload=createPayload(t,e)
  81. const response = await fetch(r, {
  82. method: "POST",
  83. headers: {
  84. "content-type": "application/json+protobuf",
  85. "x-user-agent": "grpc-web-javascript/0.1"
  86. },
  87. body: payload,
  88. mode: "cors",
  89. credentials: "omit"
  90. });
  91.  
  92. if (!response.ok) {
  93. throw new Error(`HTTP error! status: ${response.status}`);
  94. } else {
  95. return await response.json();
  96. }
  97. } catch (error) {
  98. console.error(`There was a problem with the UE function: ${error.message}`);
  99. }
  100. }
  101.  
  102. function createPayload(mode,coorData) {
  103. let payload;
  104. if (mode === 'GetMetadata') {
  105. payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData.panoId]]],[[1,2,3,4,8,6]]];
  106. } else if (mode === 'SingleImageSearch') {
  107. payload =[["apiv3",null,null,null,"US",null,null,null,null,null, [[0]]], [[null,null,coorData.lat,coorData.lng],50], [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,1,2],[3,1,2],[10,1,2]]]], [[1,2,3,4,8,6]]];
  108. } else {
  109. throw new Error("Invalid mode!");
  110. }
  111. return JSON.stringify(payload);
  112. }
  113.  
  114. function getMetaData(svData) {
  115. if (svData) {
  116. let levelId=svData.dn
  117. let year = 'noyear',month = 'nomonth'
  118. let panoType='Unofficial'
  119. let subdivision='nosub',locality='nolocality'
  120. let coverageCount='0'
  121. if (svData.imageDate) {
  122. const matchYear = svData.imageDate.match(/\d{4}/);
  123. if (matchYear) {
  124. year = matchYear[0];
  125. }
  126.  
  127. const matchMonth = svData.imageDate.match(/-(\d{2})/);
  128. if (matchMonth) {
  129. month = matchMonth[1];
  130. }
  131. }
  132.  
  133. if (svData.copyright.includes('Google')) {
  134. panoType = 'Official';
  135. }
  136. if (svData.time){
  137. coverageCount = svData.time.length.toString();
  138. }
  139. if(svData.location.description){
  140. let parts = svData.location.description.split(',');
  141. if(parts.length > 1){
  142. subdivision = parts[parts.length-1].trim();
  143. locality = parts[parts.length-2].trim();
  144. } else {
  145. subdivision = svData.location.description;
  146.  
  147. }
  148. }
  149. return [year,month,panoType,subdivision,locality,levelId,coverageCount]
  150. }
  151.  
  152. else{
  153. return null}
  154. }
  155.  
  156. function getGeneration(svData,country) {
  157.  
  158. if (svData&&svData.tiles) {
  159. if (svData.tiles.worldSize.height === 1664) { // Gen 1
  160. return 'Gen1';
  161. } else if (svData.tiles.worldSize.height === 6656) { // Gen 2 or 3
  162.  
  163. let lat;
  164. for (let key in svData.Sv) {
  165. lat = svData.Sv[key].lat;
  166. break;
  167. }
  168.  
  169. let date;
  170. if (svData.imageDate) {
  171. date = new Date(svData.imageDate);
  172. } else {
  173. date = 'nodata';
  174. }
  175.  
  176. if (date!=='nodata'&&((country === 'BD' && (date >= new Date('2021-04'))) ||
  177. (country === 'EC' && (date >= new Date('2022-03'))) ||
  178. (country === 'FI' && (date >= new Date('2020-09'))) ||
  179. (country === 'IN' && (date >= new Date('2021-10'))) ||
  180. (country === 'LK' && (date >= new Date('2021-02'))) ||
  181. (country === 'KH' && (date >= new Date('2022-10'))) ||
  182. (country === 'LB' && (date >= new Date('2021-05'))) ||
  183. (country === 'NG' && (date >= new Date('2021-06'))) ||
  184. (country === 'ST') ||
  185. (country === 'US' && lat > 52 && (date >= new Date('2019-01'))))) {
  186. return 'Shitcam';
  187. }
  188.  
  189. let gen2Countries = ['AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK', 'GR', 'RO',
  190. 'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO', 'MC', 'SM',
  191. 'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT'];
  192. if (gen2Countries.includes(country)) {
  193.  
  194. return 'Gen2or3';
  195. }
  196. else{
  197. return 'Gen3';}
  198. }
  199. else if(svData.tiles.worldSize.height === 8192){
  200. return 'Gen4';
  201. }
  202. }
  203. return 'Unknown';
  204. }
  205.  
  206. var CHUNK_SIZE = 1200;
  207. var promises = [];
  208.  
  209. async function getElevation(locations) {
  210. function findRange(elevation, ranges) {
  211. for (let i = 0; i < ranges.length; i++) {
  212. const range = ranges[i];
  213. if (elevation >= range.min && elevation <= range.max) {
  214. return `${range.min}-${range.max}m`;
  215. }
  216. }
  217. if(!elevation){
  218. return 'noElevation'
  219. }
  220. return `${JSON.stringify(elevation)}m`;
  221. }
  222.  
  223. const batchSize = 100;
  224. const totalBatches = Math.ceil(locations.length / batchSize);
  225.  
  226. for (let i = 0; i < totalBatches; i++) {
  227. const batchLocations = locations.slice(i * batchSize, (i + 1) * batchSize);
  228. const coordinates = batchLocations.map(location => `${location.lat},${location.lng}`).join('|');
  229. const url = `https://api.open-elevation.com/api/v1/lookup?locations=${coordinates}`;
  230.  
  231. try {
  232. const response = await fetch(url);
  233. const data = await response.json();
  234.  
  235. if (data && data.results && data.results.length > 0) {
  236. const elevations = data.results.map(result => result.elevation);
  237. batchLocations.forEach((location, index) => {
  238. if (location.extra && location.extra.tags) {
  239. if (sR) {
  240. const range = findRange(elevations[index], sR);
  241. location.extra.tags.push(`${range}`);
  242. } else {
  243. location.extra.tags.push(`${JSON.stringify(elevations[index])}`);
  244. }
  245. } else {
  246. location.extra = {
  247. tags: [(sR ? `${findRange(elevations[index], sR)}` : `${JSON.stringify(elevations[index])}`)]
  248. };
  249. }
  250. });
  251. } else {
  252. batchLocations.forEach(location => {
  253. if (location.extra && location.extra.tags) {
  254. location.extra.tags.push('noElevation');
  255. }
  256. else{location.extra = {
  257. tags: ['noElevation']
  258. }
  259. };
  260. });
  261. }
  262.  
  263. } catch (error) {
  264. console.log(error);
  265. }
  266. }
  267. await Promise.all(promises);
  268. return locations;
  269. }
  270. async function processCoord(coord, tags, svData,ccData) {
  271.  
  272. if (!coord.extra) {
  273. coord.extra = {};
  274. }
  275. if (!coord.extra.tags) {
  276. coord.extra.tags = [];
  277. }
  278.  
  279. if (svData){
  280. let meta=getMetaData(svData)
  281. let yearTag=meta[0]
  282. let monthTag=meta[1]
  283. let typeTag=meta[2]
  284. let subdivisionTag=meta[3]
  285. let localityTag=meta[4]
  286. let countryTag
  287. let genTag
  288. let trekkerTag=meta[5]
  289. let coverageTag=meta[6]
  290.  
  291. if (ccData){
  292. try {
  293. countryTag = ccData[1][0][5][0][1][4]
  294. }
  295. catch (error) {
  296. try {
  297. countryTag = ccData[1][5][0][1][4]
  298. } catch (error) {
  299. countryTag='nocountry'
  300. }
  301. }
  302. if (!countryTag)countryTag='nocountry'
  303. }
  304.  
  305. genTag = getGeneration(svData,countryTag)
  306. if (tags.includes('generation')&&typeTag=='Official')coord.extra.tags.push(genTag)
  307.  
  308. if (tags.includes('year'))coord.extra.tags.push(yearTag)
  309.  
  310. if (tags.includes('month'))coord.extra.tags.push(monthTag)
  311.  
  312. if (tags.includes('type'))coord.extra.tags.push(typeTag)
  313.  
  314. if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.extra.tags.push('trekker')
  315.  
  316. if (tags.includes('country')&&typeTag=='Official')coord.extra.tags.push(countryTag)
  317.  
  318. if (tags.includes('subdivision')&&typeTag=='Official')coord.extra.tags.push(subdivisionTag)
  319.  
  320. if (tags.includes('locality')&&typeTag=='Official')coord.extra.tags.push(localityTag)
  321.  
  322. if (tags.includes('coverageCount')&&typeTag=='Official')coord.extra.tags.push(coverageTag)
  323.  
  324. }
  325. else {
  326. if(tags.some(tag => tagBox.includes(tag))){
  327. coord.extra.tags.push('nopano')
  328. }
  329. }
  330.  
  331. if (coord.extra.tags) {coord.extra.tags=Array.from(new Set(coord.extra.tags))}
  332. newData.push(coord);
  333. }
  334.  
  335.  
  336. async function processChunk(chunk, tags) {
  337. if (tags.includes('elevation')){
  338. try {
  339. chunk = await getElevation(chunk,);
  340. } catch (error) {
  341. console.error('error fecthing elevtion data:', error);
  342. }
  343. }
  344. var service = new google.maps.StreetViewService();
  345. var promises = chunk.map(async coord => {
  346. let panoId = coord.panoId;
  347. if (!panoId) {
  348. if (coord.extra&&coord.extra.panoId){
  349. panoId = coord.extra.panoId;}
  350. }
  351. let latLng = {lat: coord.lat, lng: coord.lng};
  352. let svData;
  353. let ccData;
  354.  
  355. if ((panoId || latLng)) {
  356. svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50});
  357. }
  358.  
  359. if (!panoId && (tags.includes('generation')||('country'))) {
  360. ccData = await UE('SingleImageSearch', coord);
  361. } else if (panoId && (tags.includes('generation')||('country'))) {
  362. ccData = await UE('GetMetadata', coord);
  363. }
  364.  
  365. await processCoord(coord, tags, svData,ccData)
  366. });
  367.  
  368. await Promise.all(promises);
  369. }
  370.  
  371. function getSVData(service, options) {
  372. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  373. resolve(data);
  374. }));
  375. }
  376.  
  377. async function processData(tags) {
  378. try {
  379. const totalChunks = Math.ceil(data.customCoordinates.length / CHUNK_SIZE);
  380. let processedChunks = 0;
  381.  
  382. const swal = Swal.fire({
  383. title: 'Processing Data',
  384. text: 'Please wait...',
  385. allowOutsideClick: false,
  386. allowEscapeKey: false,
  387. showConfirmButton: false,
  388. didOpen: () => {
  389. Swal.showLoading();
  390. }
  391. });
  392.  
  393. for (let i = 0; i < data.customCoordinates.length; i += CHUNK_SIZE) {
  394. let chunk = data.customCoordinates.slice(i, i + CHUNK_SIZE);
  395. await processChunk(chunk, tags);
  396. processedChunks++;
  397.  
  398. const progress = Math.min((processedChunks / totalChunks) * 100, 100);
  399. Swal.update({
  400. html: `<div>${progress.toFixed(2)}% completed</div>
  401. <div class="swal2-progress">
  402. <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;">
  403. </div>
  404. </div>`
  405. });
  406. }
  407.  
  408. GM_setClipboard(JSON.stringify(newData));
  409. swal.close();
  410. Swal.fire({
  411. title: 'Success!',
  412. text: 'New JSON data has been copied to the clipboard!',
  413. icon: 'success'
  414. });
  415. } catch (error) {
  416. swal.close();
  417. Swal.fire({
  418. title: 'Error!',
  419. text: 'Invalid JSON data',
  420. icon: 'error'
  421. });
  422. console.error('Error processing JSON data:', error);
  423. }
  424. }
  425. if(data.customCoordinates){
  426. if(data.customCoordinates.length>=1){processData(tags);}
  427. else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is empty.','error');}
  428. }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');}
  429. }
  430.  
  431. function createCheckbox(text, tags) {
  432. var label = document.createElement('label');
  433. var checkbox = document.createElement('input');
  434. checkbox.type = 'checkbox';
  435. checkbox.value = text;
  436. checkbox.name = 'tags';
  437. checkbox.id = tags;
  438. label.appendChild(checkbox);
  439. label.appendChild(document.createTextNode(text));
  440. buttonContainer.appendChild(label);
  441. return checkbox;
  442. }
  443.  
  444. var mainButton = document.createElement('button');
  445. mainButton.textContent = 'Auto-Tag';
  446. mainButton.style.position = 'fixed';
  447. mainButton.style.right = '20px';
  448. mainButton.style.bottom = '20px';
  449. mainButton.style.borderRadius = "18px";
  450. mainButton.style.fontSize ="16px";
  451. mainButton.style.padding = "10px 20px";
  452. mainButton.style.border = "none";
  453. mainButton.style.color = "white";
  454. mainButton.style.cursor = "pointer";
  455. mainButton.style.backgroundColor = "#4CAF50";
  456. mainButton.addEventListener('click', function() {
  457. if (buttonContainer.style.display === 'none') {
  458. buttonContainer.style.display = 'block';
  459. } else {
  460. buttonContainer.style.display = 'none';
  461. }
  462. });
  463. document.body.appendChild(mainButton);
  464.  
  465. var buttonContainer = document.createElement('div');
  466. buttonContainer.style.position = 'fixed';
  467. buttonContainer.style.right = '20px';
  468. buttonContainer.style.bottom = '60px';
  469. buttonContainer.style.display = 'none';
  470. document.body.appendChild(buttonContainer);
  471.  
  472. var triggerButton = document.createElement('button');
  473. triggerButton.textContent = 'Star Tagging';
  474. triggerButton.addEventListener('click', function() {
  475. var checkboxes = document.getElementsByName('tags');
  476. var checkedTags = [];
  477. for (var i=0; i<checkboxes.length; i++) {
  478. if (checkboxes[i].checked) {
  479. checkedTags.push(checkboxes[i].id);
  480. }
  481. }
  482. if (checkedTags.includes('elevation')) {
  483. Swal.fire({
  484. title: 'Set A Range For Elevation',
  485. text: 'Please set a range for the elevation. If you select "Cancel", the script will return the exact elevation for each location.',
  486. icon: 'question',
  487. showCancelButton: true,
  488. showCloseButton: true,
  489. allowOutsideClick: false,
  490. confirmButtonColor: '#3085d6',
  491. cancelButtonColor: '#d33',
  492. confirmButtonText: 'Yes',
  493. cancelButtonText: 'Cancel'
  494. }).then((result) => {
  495. if (result.isConfirmed){
  496. Swal.fire({
  497. title: 'Define Range for Each Segment',
  498. html: `
  499. <label> <br>Enter range for each segment, separated by commas</br></label>
  500. <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea>
  501. `,
  502. icon: 'question',
  503. showCancelButton: true,
  504. showCloseButton: true,
  505. allowOutsideClick: false,
  506. focusConfirm: false,
  507. preConfirm: () => {
  508. const segmentRangesInput = document.getElementById('segmentRanges').value.trim();
  509. if (!segmentRangesInput) {
  510. Swal.showValidationMessage('Please enter range for each segment');
  511. return false;
  512. }
  513. const segmentRanges = segmentRangesInput.split(',');
  514. const validatedRanges = segmentRanges.map(range => {
  515. const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/);
  516. if (matches) {
  517. const min = Number(matches[1]);
  518. const max = Number(matches[2]);
  519. return { min, max };
  520. } else {
  521. Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue');
  522. return false;
  523. }
  524. });
  525. return validatedRanges.filter(Boolean);
  526. },
  527. confirmButtonColor: '#3085d6',
  528. cancelButtonColor: '#d33',
  529. confirmButtonText: 'Yes',
  530. cancelButtonText: 'Cancel',
  531. inputValidator: (value) => {
  532. if (!value.trim()) {
  533. return 'Please enter range for each segment';
  534. }
  535. }
  536. }).then((result) => {
  537. if (result.isConfirmed) {
  538. runScript(checkedTags,result.value)
  539. } else {
  540. Swal.showValidationMessage('You canceled input');
  541. }
  542. });}
  543. });
  544. }
  545. else{
  546. runScript(checkedTags)}
  547. })
  548. buttonContainer.appendChild(triggerButton);
  549.  
  550. createCheckbox('Year', 'year');
  551. createCheckbox('Month', 'month');
  552. createCheckbox('Type', 'type');
  553. createCheckbox('Country', 'country');
  554. createCheckbox('Subdivision', 'subdivision');
  555. createCheckbox('Locality', 'locality');
  556. createCheckbox('Generations', 'generation');
  557. createCheckbox('Coverage Count','coverageCount')
  558. createCheckbox('Elevation','elevation')
  559. })();