Geoguessr Map-Making Auto-Tag

Tag your panoramas by date, exactTime, address, generation, elevation

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

  1. // ==UserScript==
  2. // @name Geoguessr Map-Making Auto-Tag
  3. // @namespace https://greasyfork.org/users/1179204
  4. // @version 3.85.7
  5. // @description Tag your panoramas by date, exactTime, address, generation, elevation
  6. // @author KaKa
  7. // @match *://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.svgrepo.com/show/423677/tag-price-label.svg
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17. let accuracy=60 /* You could modifiy accuracy here, default setting is 60s */
  18. let tagBox = ['Year', 'Month','Day', 'Time','Country', 'Subdivision', 'Locality', 'Generation', 'Elevation','Type','Fix','Detect']
  19. let months = ['January', 'February', 'March', 'April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December'];
  20. let mapData
  21.  
  22. function getMap() {
  23. return new Promise(function(resolve, reject) {
  24. var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
  25. fetch(requestURL, {
  26. headers: {
  27. 'Accept': 'application/json',
  28. 'Content-Encoding':'gzip'
  29. }
  30. })
  31. .then(function(response) {
  32. if (!response.ok) {
  33. throw new Error('HTTP error, status = ' + response.status);
  34. }
  35. return response.json();
  36. })
  37. .then(function(jsonData) {
  38. resolve(jsonData);
  39. })
  40. .catch(function(error) {
  41. console.error('Fetch Error:', error);
  42. reject('Error fetching meta data of the map!');
  43. });
  44. });
  45. }
  46.  
  47. async function getSelection() {
  48. return new Promise((resolve, reject) => {
  49. var exportButtonText = 'Export';
  50. var buttons = document.querySelectorAll('button.button');
  51.  
  52. for (var i = 0; i < buttons.length; i++) {
  53. if (buttons[i].textContent.trim() === exportButtonText) {
  54. buttons[i].click();
  55. var modalDialog = document.querySelector('.modal__dialog.export-modal');
  56. }
  57. }
  58.  
  59. setTimeout(() => {
  60. const radioButton = document.querySelector('input[type="radio"][name="selection"][value="1"]');
  61. const spanText = radioButton.nextElementSibling.textContent.trim();
  62. if (spanText==="Export selection (0 locations)") {
  63. swal.fire('Selection not found!', 'Please select at least one location as selection!','warning')
  64. reject(new Error('Export selection is empty!'));
  65. }
  66. if (radioButton) radioButton.click()
  67. else{
  68. reject(new Error('Radio button not found'));}
  69. }, 100);
  70.  
  71.  
  72. setTimeout(() => {
  73. const copyButton = document.querySelector('.export-modal__export-buttons button:first-of-type');
  74. if (!copyButton) {
  75. reject(new Error('Copy button not found'));
  76. }
  77. copyButton.click();
  78.  
  79. }, 200);
  80. setTimeout(() => {
  81. const closeButton = document.querySelector('.modal__close');
  82. if (closeButton) closeButton.click();
  83. else reject(new Error('Close button not found'));
  84. }, 400);
  85.  
  86. setTimeout(async () => {
  87. try {
  88. const data = await navigator.clipboard.readText()
  89. const selection = JSON.parse(data);
  90. resolve(selection);
  91. } catch (error) {
  92. console.error("Error getting selection:", error);
  93. reject(error);
  94. }
  95. }, 800);
  96. });
  97. }
  98.  
  99.  
  100. function matchSelection(selection, locations) {
  101. const matchingLocations = [];
  102. const customCoordinates = selection.customCoordinates;
  103.  
  104. const locationMap = {};
  105. locations.forEach(loc => {
  106. const locString = JSON.stringify(loc.location);
  107. locationMap[locString] = loc;
  108. });
  109.  
  110. for (const coord of customCoordinates) {
  111. const coordString = JSON.stringify({ lat: coord.lat, lng: coord.lng });
  112. if (locationMap.hasOwnProperty(coordString)) {
  113. const matchingLoc = locationMap[coordString];
  114. if (coord.extra.hasOwnProperty('panoDate') && coord.extra.panoDate) {
  115. matchingLoc.panoDate = coord.panoDate;
  116. }
  117. matchingLocations.push(matchingLoc);
  118. }
  119. }
  120.  
  121. return matchingLocations;
  122. }
  123.  
  124. function findRange(elevation, ranges) {
  125. for (let i = 0; i < ranges.length; i++) {
  126. const range = ranges[i];
  127. if (elevation >= range.min && elevation <= range.max) {
  128. return `${range.min}-${range.max}m`;
  129. }
  130. }
  131. if (!elevation) {
  132. return 'noElevation';
  133. }
  134. return `${JSON.stringify(elevation)}m`;
  135. }
  136.  
  137. function updateSelection(entries) {
  138. var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
  139. var payload = {
  140. edits: []
  141. };
  142.  
  143. entries.forEach(function(entry) {
  144. var createEntry = {
  145. id: -1,
  146. author: entry.author,
  147. mapId: entry.mapId,
  148. location: entry.location,
  149. panoId: entry.panoId,
  150. panoDate: entry.panoDate,
  151. heading: entry.heading,
  152. pitch: entry.pitch,
  153. zoom: entry.zoom,
  154. tags: entry.tags,
  155. flags: entry.flags,
  156. createdAt: entry.createdAt,
  157.  
  158. };
  159. payload.edits.push({
  160. action: {
  161. type: 3
  162. },
  163. create: [createEntry],
  164. remove: [entry.id]
  165. });
  166. });
  167.  
  168. var xhr = new XMLHttpRequest();
  169. xhr.open("POST", requestURL);
  170. xhr.setRequestHeader("Content-Type", "application/json");
  171.  
  172. xhr.onload = function() {
  173. if (xhr.status >= 200 && xhr.status < 300) {
  174. console.log("Request succeeded");
  175. } else {
  176. console.error("Request failed with status", xhr.status);
  177. }
  178. };
  179.  
  180. xhr.onerror = function() {
  181. swal.fire({
  182. icon: 'error',
  183. title: 'Oops...',
  184. text: 'Failed to update the map! Please retrieve JSON data from your clipboard.'
  185. });
  186. };
  187.  
  188. xhr.send(JSON.stringify(payload));
  189. }
  190.  
  191. async function runScript(tags,sR) {
  192. let taggedLocs=[];
  193. let exportMode,selections,fixStrategy
  194.  
  195. if (tags.length<1){
  196. swal.fire('Feature not found!', 'Please select at least one feature!','warning')
  197. return}
  198. if (tags.includes('fix')){
  199. const { value: fixOption,dismiss: fixDismiss } = await Swal.fire({
  200. title:'Fix Strategy',
  201. icon:'question',
  202. text: 'Would you like to fix the location based on the map-making data. (more suitable for those locs with a specific date coverage) Else it will update the broken loc with recent coverage.',
  203. showCancelButton: true,
  204. showCloseButton:true,
  205. allowOutsideClick: false,
  206. confirmButtonColor: '#3085d6',
  207. cancelButtonColor: '#d33',
  208. confirmButtonText: 'Yes',
  209. cancelButtonText: 'No',
  210.  
  211. })
  212. if(fixOption)fixStrategy='exactly'
  213. else if(!fixOption&&fixDismiss==='cancel'){
  214. fixStrategy=null
  215. }
  216. else{
  217. return
  218. }
  219. };
  220.  
  221. const { value: option,dismiss: inputDismiss } = await Swal.fire({
  222. title: 'Export',
  223. text: 'Do you want to update and save your map? If you click "Cancel", the script will just paste JSON data to the clipboard after finish tagging.',
  224. icon: 'question',
  225. showCancelButton: true,
  226. showCloseButton:true,
  227. allowOutsideClick: false,
  228. confirmButtonColor: '#3085d6',
  229. cancelButtonColor: '#d33',
  230. confirmButtonText: 'Yes',
  231. cancelButtonText: 'Cancel'
  232. });
  233.  
  234. if (option) {
  235. exportMode='save'
  236. }
  237. else if(!selections&&inputDismiss==='cancel'){
  238. exportMode=null
  239. }
  240. else{
  241. return
  242. }
  243.  
  244. const loadingSwal = Swal.fire({
  245. title: 'Preparing',
  246. text: 'Fetching selected locs from map-making. Please wait...',
  247. allowOutsideClick: false,
  248. allowEscapeKey: false,
  249. showConfirmButton: false,
  250. icon:"info",
  251. didOpen: () => {
  252. Swal.showLoading();
  253. }
  254. });
  255. const selectedLocs=await getSelection()
  256. mapData=await getMap()
  257. selections=await matchSelection(selectedLocs,mapData)
  258. loadingSwal.close()
  259. async function UE(t, e, s, d) {
  260. try {
  261. const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
  262. let payload = createPayload(t, e,s,d);
  263.  
  264. const response = await fetch(r, {
  265. method: "POST",
  266. headers: {
  267. "content-type": "application/json+protobuf",
  268. "x-user-agent": "grpc-web-javascript/0.1"
  269. },
  270. body: payload,
  271. mode: "cors",
  272. credentials: "omit"
  273. });
  274.  
  275. if (!response.ok) {
  276. throw new Error(`HTTP error! status: ${response.status}`);
  277. } else {
  278. return await response.json();
  279. }
  280. } catch (error) {
  281. console.error(`There was a problem with the UE function: ${error.message}`);
  282. }
  283. }
  284.  
  285. function createPayload(mode,coorData,s,d,r) {
  286. let payload;
  287. if(!r)r=50 // default search radius
  288. if (mode === 'GetMetadata') {
  289. payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]];
  290. } else if (mode === 'SingleImageSearch') {
  291. var lat = coorData.lat;
  292. var lng = coorData.lng;
  293. lat = lat % 1 !== 0 && lat.toString().split('.')[1].length >6 ? parseFloat(lat.toFixed(6)) : lat;
  294. lng = lng % 1 !== 0 && lng.toString().split('.')[1].length > 6 ? parseFloat(lng.toFixed(6)) : lng;
  295. if(s&&d){
  296. payload=[["apiv3"],[[null,null,lat,lng],r],[[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]]]
  297. }else{
  298. payload =[["apiv3",null,null,null,"US",null,null,null,null,null, [[0]]],
  299. [[null,null,lat,lng],r],
  300. [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]]];}
  301. } else {
  302. throw new Error("Invalid mode!");
  303. }
  304. return JSON.stringify(payload);
  305. }
  306.  
  307. function monthToTimestamp(m) {
  308.  
  309. const [year, month] = m.split('-');
  310.  
  311. const startDate =Math.round( new Date(year, month-1,1).getTime()/1000);
  312.  
  313. const endDate =Math.round( new Date(year, month, 1).getTime()/1000)-1;
  314.  
  315. return { startDate, endDate };
  316. }
  317.  
  318. async function binarySearch(c, start,end) {
  319. let capture
  320. let response
  321. while (end - start >= accuracy) {
  322. let mid= Math.round((start + end) / 2);
  323. response = await UE("SingleImageSearch", c, start,end,10);
  324. if (response&&response[0][2]== "Search returned no images." ){
  325. start=mid+start-end
  326. end=start-mid+end
  327. mid=Math.round((start+end)/2)
  328. } else {
  329. start=mid
  330. mid=Math.round((start+end)/2)
  331. }
  332. capture=mid
  333. }
  334.  
  335. return capture
  336. }
  337.  
  338. function getMetaData(svData) {
  339. let year = 'Year not found',month = 'Month not found'
  340. let panoType='unofficial'
  341. let subdivision='Subdivision not found',locality='Locality not found'
  342. if (svData) {
  343. if (svData.imageDate) {
  344. const matchYear = svData.imageDate.match(/\d{4}/);
  345. if (matchYear) {
  346. year = matchYear[0];
  347. }
  348.  
  349. const matchMonth = svData.imageDate.match(/-(\d{2})/);
  350. if (matchMonth) {
  351. month = matchMonth[1];
  352. }
  353. }
  354. if (svData.copyright.includes('Google')) {
  355. panoType = 'Official';
  356. }
  357.  
  358. if(svData.location.description){
  359. let parts = svData.location.description.split(',');
  360. if(parts.length > 1){
  361. subdivision = parts[parts.length-1].trim();
  362. locality = parts[parts.length-2].trim();
  363. } else {
  364. subdivision = svData.location.description;
  365.  
  366. }
  367. }
  368. return [year,month,panoType,subdivision,locality]
  369. }
  370. else{
  371. return null
  372. }
  373. }
  374.  
  375. function extractDate(array) {
  376. var year,month
  377. array.forEach(element => {
  378. const yearRegex1 = /^(\d{2})-(\d{2})$/;
  379. const yearRegex2 = /^(\d{4})-(\d{2})$/;
  380. const yearRegex3=/^(\d{4})$/;
  381. const monthRegex1 = /^(\d{2})$/;
  382. const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i;
  383.  
  384. if (!year &&!month&& yearRegex1.test(element)) {
  385. const match = yearRegex1.exec(element);
  386. year = parseInt(match[1]) + 2000;
  387. month = parseInt(match[2]);
  388. }
  389.  
  390. if (!year && !month&& yearRegex2.test(element)) {
  391. const match = yearRegex2.exec(element);
  392. year = parseInt(match[1]);
  393. month = parseInt(match[2]);
  394. }
  395. if(!year&& yearRegex3.test(element)){
  396. const match = yearRegex3.test(element);
  397. year=parseInt(element)
  398. }
  399. if (!month && monthRegex1.test(element)) {
  400. month= parseInt(element);
  401. }
  402.  
  403. if (!month && monthRegex2.test(element)) {
  404. const months = {
  405. "January": 1, "February": 2, "March": 3, "April": 4,
  406. "May": 5, "June": 6, "July": 7, "August": 8,
  407. "September": 9, "October": 10, "November": 11, "December": 12
  408. };
  409. month = months[element];
  410. }
  411. });
  412. return {year,month}
  413. }
  414.  
  415. function getGeneration(svData,country) {
  416. if (svData&&svData.tiles) {
  417. if (svData.tiles.worldSize.height === 1664) { // Gen 1
  418. return 'Gen1';
  419. } else if (svData.tiles.worldSize.height === 6656) { // Gen 2 or 3
  420.  
  421. let lat;
  422. for (let key in svData.Sv) {
  423. lat = svData.Sv[key].lat;
  424. break;
  425. }
  426.  
  427. let date;
  428. if (svData.imageDate) {
  429. date = new Date(svData.imageDate);
  430. } else {
  431. date = 'nodata';
  432. }
  433.  
  434. if (date!=='nodata'&&((country === 'BD' && (date >= new Date('2021-04'))) ||
  435. (country === 'EC' && (date >= new Date('2022-03'))) ||
  436. (country === 'FI' && (date >= new Date('2020-09'))) ||
  437. (country === 'IN' && (date >= new Date('2021-10'))) ||
  438. (country === 'LK' && (date >= new Date('2021-02'))) ||
  439. (country === 'KH' && (date >= new Date('2022-10'))) ||
  440. (country === 'LB' && (date >= new Date('2021-05'))) ||
  441. (country === 'NG' && (date >= new Date('2021-06'))) ||
  442. (country === 'ST') ||
  443. (country === 'US' && lat > 52 && (date >= new Date('2019-01'))))) {
  444. return 'Shitcam';
  445. }
  446.  
  447. let gen2Countries = ['AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK', 'GR', 'RO',
  448. 'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO', 'MC', 'SM',
  449. 'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT'];
  450. if (gen2Countries.includes(country)) {
  451.  
  452. return 'Gen2or3';
  453. }
  454. else{
  455. return 'Gen3';}
  456. }
  457. else if(svData.tiles.worldSize.height === 8192){
  458. return 'Gen4';
  459. }
  460. }
  461. return 'Unknown';
  462. }
  463.  
  464. async function getLocal(coord, timestamp) {
  465. const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
  466. try {
  467. const [lat, lng] = coord;
  468. const url = `https://api.wheretheiss.at/v1/coordinates/${lat},${lng}`;
  469.  
  470. const response = await fetch(url);
  471. if (!response.ok) {
  472. throw new Error("Request failed: " + response.statusText);
  473. }
  474. const data = await response.json();
  475. const targetTimezoneOffset = data.offset * 3600;
  476. const offsetDiff = systemTimezoneOffset - targetTimezoneOffset;
  477. const convertedTimestamp = Math.round(timestamp - offsetDiff);
  478. return convertedTimestamp;
  479. } catch (error) {
  480. throw error;
  481. }
  482. }
  483.  
  484. var CHUNK_SIZE = 1200;
  485. if (tags.includes('time')){
  486. CHUNK_SIZE = 500
  487. }
  488. var promises = [];
  489.  
  490. async function processCoord(coord, tags, svData,ccData) {
  491.  
  492. try{
  493. if (svData||ccData){
  494. var panoYear,panoMonth
  495. if (coord.panoDate){
  496. panoYear=parseInt(coord.panoDate.substring(0,4))
  497. panoMonth=parseInt(coord.panoDate.substring(5,7))
  498. }
  499. else{
  500. panoYear=parseInt(extractDate(coord.tags).year)
  501. panoMonth=parseInt(extractDate(coord.tags).month)
  502. }
  503.  
  504. let meta=getMetaData(svData)
  505. let yearTag=meta[0]
  506. let monthTag=parseInt(meta[1])
  507. let typeTag=meta[2]
  508. let subdivisionTag=meta[3]
  509. let localityTag=meta[4]
  510. let countryTag,elevationTag
  511. let genTag,trekkerTag,floorTag
  512. let dayTag,timeTag,exactTime,timeRange
  513.  
  514. if(monthTag){monthTag=months[monthTag-1]}
  515. if (!monthTag){monthTag='Month not found'}
  516.  
  517. var date=monthToTimestamp(svData.imageDate)
  518.  
  519. if(tags.includes('day')||tags.includes('time')){
  520. const initialSearch=await UE('SingleImageSearch',{'lat':coord.location.lat,'lng':coord.location.lng},date.startDate,date.endDate)
  521. if (initialSearch){
  522. if (initialSearch.length!=3)exactTime=null;
  523. else{
  524. exactTime=await binarySearch({'lat':coord.location.lat,'lng':coord.location.lng}, date.startDate,date.endDate)
  525. }
  526. }
  527.  
  528.  
  529. }
  530.  
  531. if(!exactTime){dayTag='Day not found'
  532. timeTag='Time not found'
  533. }
  534. else{
  535.  
  536. const currentDate = new Date();
  537. const currentOffset =-(currentDate.getTimezoneOffset())*60
  538. const dayOffset = currentOffset-Math.round((coord.location.lng / 15) * 3600);
  539. const LocalDay=new Date(Math.round(exactTime-dayOffset)*1000)
  540. dayTag = LocalDay.toISOString().split('T')[0];
  541.  
  542. if(tags.includes('time')) {
  543.  
  544. var localTime=await getLocal([coord.location.lat,coord.location.lng],exactTime)
  545. var timeObject=new Date(localTime*1000)
  546. timeTag =`${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`;
  547. var hour = timeObject.getHours();
  548.  
  549. if (hour < 11) {
  550. timeRange = 'Morning';
  551. } else if (hour >= 11 && hour < 13) {
  552. timeRange = 'Noon';
  553. } else if (hour >= 13 && hour < 17) {
  554. timeRange = 'Afternoon';
  555. } else if(hour >= 17 && hour < 19) {
  556. timeRange = 'Evening';
  557. }
  558. else{
  559. timeRange = 'Night';
  560. }
  561. }
  562. }
  563.  
  564. try {if (ccData.length!=3) ccData=ccData[1][0]
  565. else ccData=ccData[1]
  566. }
  567.  
  568. catch (error) {
  569. ccData=null
  570. }
  571.  
  572. if (ccData){
  573. try{
  574. countryTag = ccData[5][0][1][4]}
  575. catch(error){
  576. countryTag=null
  577. }
  578. try{
  579. elevationTag=ccData[5][0][1][1][0]}
  580. catch(error){
  581. elevationTag=null
  582. }
  583. try{
  584. trekkerTag=ccData[6][5]}
  585. catch(error){
  586. trekkerTag=null
  587. }
  588. try{
  589. floorTag=ccData[5][0][1][3][2][0]
  590. }
  591. catch(error){
  592. floorTag=null
  593. }
  594. if (tags.includes('detect')){
  595. const defaultDate=3
  596. }
  597. }
  598.  
  599. if (trekkerTag){
  600. trekkerTag=trekkerTag.toString()
  601. if( trekkerTag.includes('scout')){
  602. trekkerTag='trekker'
  603. }}
  604.  
  605. if(elevationTag){
  606. elevationTag=Math.round(elevationTag*100)/100
  607. if(sR){
  608. elevationTag=findRange(elevationTag,sR)
  609. }
  610. else{
  611. elevationTag=elevationTag.toString()+'m'
  612. }
  613. }
  614.  
  615. if (!countryTag)countryTag='Country not found'
  616. if (!elevationTag)elevationTag='Elevation not found'
  617.  
  618. if (tags.includes('generation')&&typeTag=='Official'&&countryTag){
  619. genTag = getGeneration(svData,countryTag)
  620. coord.tags.push(genTag)}
  621.  
  622. if (tags.includes('year'))coord.tags.push(yearTag)
  623.  
  624. if (tags.includes('month'))coord.tags.push(monthTag)
  625.  
  626. if (tags.includes('day'))coord.tags.push(dayTag)
  627.  
  628. if (tags.includes('time')) coord.tags.push(timeTag)
  629.  
  630. if (tags.includes('time')&&timeRange) coord.tags.push(timeRange)
  631.  
  632. if (tags.includes('type'))coord.tags.push(typeTag)
  633.  
  634. if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.tags.push('trekker')
  635.  
  636. if (tags.includes('type')&&floorTag&&typeTag=='Official')coord.tags.push(floorTag)
  637.  
  638. if (tags.includes('country'))coord.tags.push(countryTag)
  639.  
  640. if (tags.includes('subdivision')&&typeTag=='Official')coord.tags.push(subdivisionTag)
  641.  
  642. if (tags.includes('locality')&&typeTag=='Official')coord.tags.push(localityTag)
  643.  
  644. if (tags.includes('elevation'))coord.tags.push(elevationTag)
  645.  
  646. if (tags.includes('fix')){
  647. if (!fixStrategy){
  648. try{
  649. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,10)
  650. const updatedPnaoId=resultPano[1][1][1]
  651. if (coord.panoId){
  652. if (updatedPnaoId&&updatedPnaoId!=coord.panoId) {
  653. coord.panoId=updatedPnaoId
  654. coord.tags.push('Updated')
  655. }}
  656. else{
  657. const updatedYear=resultPano[1][6][7][0]
  658. const updatedMonth=resultPano[1][6][7][1]
  659. if (panoYear&&panoMonth&&updatedYear&&updatedMonth){
  660. if(panoYear!=updatedYear||panoMonth!=updatedMonth){
  661. coord.panoId=updatedPnaoId
  662. coord.tags.push('Updated')
  663. }
  664. }
  665. else{
  666. coord.tags.push('Failed to update')
  667. }
  668. }
  669. }
  670. catch (error){
  671. coord.tags.push('Failed to update')
  672. }
  673. }
  674. }
  675. }
  676. }
  677. catch (error) {
  678. if(!tags.includes('fix'))coord.tags.push('Pano not found');
  679. else{
  680. var fixState
  681. try{
  682. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,5)
  683. if(fixStrategy){
  684. const panos=resultPano[1][5][0][8]
  685. for(const pano of panos){
  686. if((pano[1][0]===panoYear&&pano[1][1]===panoMonth)){
  687. const panoIndex=pano[0]
  688. const fixedPanoId=resultPano[1][5][0][3][0][panoIndex][0][1]
  689. coord.panoId=fixedPanoId
  690. coord.location.lat=resultPano[1][5][0][1][0][2]
  691. coord.location.lng=resultPano[1][5][0][1][0][3]
  692. fixState=true
  693. }
  694. }
  695. }
  696. else{
  697. coord.panoId=resultPano[1][1][1]
  698. fixState=true
  699. }
  700.  
  701. }
  702. catch (error){
  703. fixState=null
  704. }
  705. if (!fixState)coord.tags.push('Failed to fix')
  706. else coord.tags.push('Fixed')
  707.  
  708. }
  709. }
  710.  
  711. if (coord.tags) {coord.tags=Array.from(new Set(coord.tags))}
  712. taggedLocs.push(coord);
  713. }
  714.  
  715. async function processChunk(chunk, tags) {
  716. var service = new google.maps.StreetViewService();
  717. var promises = chunk.map(async coord => {
  718. let panoId = coord.panoId;
  719. let latLng = {lat: coord.location.lat, lng: coord.location.lng};
  720. let svData;
  721. let ccData;
  722. if ((panoId || latLng)) {
  723. if(tags!=['country']&&tags!=['elevation']&&tags!=['detect']){
  724. svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50});}
  725. }
  726.  
  727. if (!panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  728. ccData = await UE('SingleImageSearch', latLng);
  729. }
  730.  
  731. else if (panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  732. ccData = await UE('GetMetadata', panoId);
  733. }
  734.  
  735. if (latLng && (tags.includes('detect'))) {
  736. var detectYear,detectMonth
  737. if (coord.panoDate){
  738. detectYear=parseInt(coord.panoDate.substring(0,4))
  739. detectMonth=parseInt(coord.panoDate.substring(5,7))
  740. }
  741. else{
  742. if(coord.panoId){
  743. const metaData=await getSVData(service,{pano: panoId})
  744. if (metaData){
  745. if(metaData.imageDate){
  746. detectYear=parseInt(metaData.imageDate.substring(0,4))
  747. detectMonth=parseInt(metaData.imageDate.substring(5,7))
  748. }
  749. }
  750. }
  751. }
  752. if (detectYear&&detectMonth){
  753. const metaData = await UE('SingleImageSearch', latLng,10);
  754. if (metaData){
  755. if(metaData.length>1){
  756. const defaultDate=metaData[1][6][7]
  757. if (defaultDate[0]===detectYear&&defaultDate[1]!=detectMonth){
  758. coord.tags.push('Dangerous')}
  759. }
  760. }
  761. }
  762. }
  763. if (tags!=['detect']){
  764. await processCoord(coord, tags, svData,ccData)}
  765. });
  766. await Promise.all(promises);
  767.  
  768. }
  769.  
  770. function getSVData(service, options) {
  771. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  772. resolve(data);
  773.  
  774. }));
  775. }
  776.  
  777. async function processData(tags) {
  778. let successText='The JSON data has been pasted to your clipboard!';
  779. try {
  780. const totalChunks = Math.ceil(selections.length / CHUNK_SIZE);
  781. let processedChunks = 0;
  782.  
  783. const swal = Swal.fire({
  784. title: 'Tagging',
  785. text: 'If you try to tag a large number of locs by exact time or elevation, it could take quite some time. Please wait...',
  786. allowOutsideClick: false,
  787. allowEscapeKey: false,
  788. showConfirmButton: false,
  789. icon:"info",
  790. didOpen: () => {
  791. Swal.showLoading();
  792. }
  793. });
  794.  
  795. for (let i = 0; i < selections.length; i += CHUNK_SIZE) {
  796. let chunk = selections.slice(i, i + CHUNK_SIZE);
  797. await processChunk(chunk, tags);
  798. processedChunks++;
  799.  
  800. const progress = Math.min((processedChunks / totalChunks) * 100, 100);
  801. Swal.update({
  802. html: `<div>${progress.toFixed(2)}% completed</div>
  803. <div class="swal2-progress">
  804. <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;">
  805. </div>
  806. </div>`
  807. });
  808. if(exportMode){
  809. updateSelection(chunk)
  810. successText='Tagging completed! Do you want to refresh the page?(The JSON data is also pasted to your clipboard)'
  811. }
  812. }
  813.  
  814. swal.close();
  815. var newJSON=[]
  816. taggedLocs.forEach((loc)=>{
  817. newJSON.push({lat:loc.location.lat,
  818. lng:loc.location.lng,
  819. heading:loc.heading,
  820. pitch: loc.pitch !== undefined && loc.pitch !== null ? loc.pitch : 90,
  821. zoom: loc.zoom !== undefined && loc.zoom !== null ? loc.zoom : 0,
  822. panoId:loc.panoId,
  823. extra:{tags:loc.tags}
  824. })
  825. })
  826. GM_setClipboard(JSON.stringify(newJSON))
  827. Swal.fire({
  828. title: 'Success!',
  829. text: successText,
  830. icon: 'success',
  831. showCancelButton: true,
  832. confirmButtonColor: '#3085d6',
  833. cancelButtonColor: '#d33',
  834. confirmButtonText: 'OK'
  835. }).then((result) => {
  836. if (result.isConfirmed) {
  837. if(exportMode){
  838. location.reload();}
  839. }
  840. });
  841. } catch (error) {
  842. swal.close();
  843. Swal.fire('Error Tagging!', '','error');
  844. console.error('Error processing JSON data:', error);
  845. }
  846. }
  847.  
  848. if(selections){
  849. if(selections.length>=1){processData(tags);}
  850. else{
  851. Swal.fire('Error Parsing JSON Data!', 'The input JSON data is empty! If you update the map after the page is loaded, please save it and refresh the page before tagging','error');}
  852. }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');}
  853. }
  854.  
  855. function createCheckbox(text, tags) {
  856. var label = document.createElement('label');
  857. var checkbox = document.createElement('input');
  858. checkbox.type = 'checkbox';
  859. checkbox.value = text;
  860. checkbox.name = 'tags';
  861. checkbox.id = tags;
  862. checkbox.style.marginLeft='2px'
  863. label.appendChild(checkbox);
  864. label.appendChild(document.createTextNode(text));
  865. buttonContainer.appendChild(label);
  866. return checkbox;
  867. }
  868. const elementContainer=document.getElementsByClassName('tool-block')[0];
  869. var mainButton = document.createElement('button');
  870. mainButton.textContent = 'Auto-Tag';
  871. mainButton.id='main-button';
  872. mainButton.style.position = 'fixed';
  873. mainButton.style.right = '20px';
  874. mainButton.style.bottom = '15px';
  875. mainButton.style.borderRadius = "18px";
  876. mainButton.style.fontSize = "15px";
  877. mainButton.style.padding = "10px 20px";
  878. mainButton.style.border = "none";
  879. mainButton.style.color = "white";
  880. mainButton.style.cursor = "pointer";
  881. mainButton.style.backgroundColor = "#4CAF50";
  882. mainButton.addEventListener('click', function () {
  883. if (buttonContainer.style.display === 'none') {
  884. buttonContainer.style.display = 'block';
  885. } else {
  886. buttonContainer.style.display = 'none';
  887. }
  888. });
  889. document.body.appendChild(mainButton);
  890.  
  891. var buttonContainer = document.createElement('div');
  892. buttonContainer.style.position = 'fixed';
  893. buttonContainer.id='button-container';
  894. buttonContainer.style.right = '20px';
  895. buttonContainer.style.bottom = '50px';
  896. buttonContainer.style.display = 'none';
  897. buttonContainer.style.fontSize = '14px';
  898. document.body.appendChild(buttonContainer);
  899.  
  900. var selectAllCheckbox = document.createElement('input');
  901. selectAllCheckbox.type = 'checkbox';
  902. selectAllCheckbox.id = 'selectAll';
  903. selectAllCheckbox.addEventListener('change', function () {
  904. var checkboxes = document.getElementsByName('tags');
  905. for (var i = 0; i < checkboxes.length; i++) {
  906. if (!['Fix', 'Detect'].includes(checkboxes[i].value)){
  907. checkboxes[i].checked = this.checked;
  908. }
  909. }
  910. });
  911. var selectAllLabel = document.createElement('label');
  912. selectAllLabel.textContent = 'Select All';
  913. selectAllLabel.htmlFor = 'selectAll';
  914.  
  915.  
  916.  
  917. var triggerButton = document.createElement('button');
  918. triggerButton.textContent = 'Start Tagging';
  919. triggerButton.addEventListener('click', function () {
  920. var checkboxes = document.getElementsByName('tags');
  921. var checkedTags = [];
  922. for (var i = 0; i < checkboxes.length; i++) {
  923. if (checkboxes[i].checked) {
  924. checkedTags.push(checkboxes[i].id);
  925. }
  926. }
  927. if (checkedTags.includes('elevation')) {
  928. Swal.fire({
  929. title: 'Set A Range For Elevation',
  930. text: 'If you select "Cancel", the script will return the exact elevation for each location.',
  931. icon: 'question',
  932. showCancelButton: true,
  933. showCloseButton: true,
  934. allowOutsideClick: false,
  935. confirmButtonColor: '#3085d6',
  936. cancelButtonColor: '#d33',
  937. confirmButtonText: 'Yes',
  938. cancelButtonText: 'Cancel'
  939. }).then((result) => {
  940. if (result.isConfirmed) {
  941. Swal.fire({
  942. title: 'Define Range for Each Segment',
  943. html: `
  944. <label> <br>Enter range for each segment, separated by commas</br></label>
  945. <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea>
  946. `,
  947. icon: 'question',
  948. showCancelButton: true,
  949. showCloseButton: true,
  950. allowOutsideClick: false,
  951. focusConfirm: false,
  952. preConfirm: () => {
  953. const segmentRangesInput = document.getElementById('segmentRanges').value.trim();
  954. if (!segmentRangesInput) {
  955. Swal.showValidationMessage('Please enter range for each segment');
  956. return false;
  957. }
  958. const segmentRanges = segmentRangesInput.split(',');
  959. const validatedRanges = segmentRanges.map(range => {
  960. const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/);
  961. if (matches) {
  962. const min = Number(matches[1]);
  963. const max = Number(matches[2]);
  964. return { min, max };
  965. } else {
  966. Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue');
  967. return false;
  968. }
  969. });
  970. return validatedRanges.filter(Boolean);
  971. },
  972. confirmButtonColor: '#3085d6',
  973. cancelButtonColor: '#d33',
  974. confirmButtonText: 'Yes',
  975. cancelButtonText: 'Cancel',
  976. inputValidator: (value) => {
  977. if (!value.trim()) {
  978. return 'Please enter range for each segment';
  979. }
  980. }
  981. }).then((result) => {
  982. if (result.isConfirmed) {
  983. runScript(checkedTags, result.value)
  984. } else {
  985. Swal.showValidationMessage('You canceled input');
  986. }
  987. });
  988. }
  989. else if (result.dismiss === Swal.DismissReason.cancel) {
  990. runScript(checkedTags)
  991. }
  992. });
  993. }
  994. else {
  995. runScript(checkedTags)
  996. }
  997. })
  998. buttonContainer.appendChild(triggerButton);
  999. buttonContainer.appendChild(selectAllCheckbox);
  1000. buttonContainer.appendChild(selectAllLabel);
  1001. tagBox.forEach(tag => {
  1002. createCheckbox(tag, tag.toLowerCase());
  1003. });
  1004.  
  1005. })();