Geoguessr Map-Making Auto-Tag

Tag your street views by date&address&generations

目前为 2024-05-23 提交的版本,查看 最新版本

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