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.8
  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"],
  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.  
  495. var panoYear,panoMonth
  496. if (coord.panoDate){
  497. panoYear=parseInt(coord.panoDate.substring(0,4))
  498. panoMonth=parseInt(coord.panoDate.substring(5,7))
  499. }
  500. else if(coord.panoId){
  501. panoYear=parseInt(svData.imageDate.substring(0,4))
  502. panoMonth=parseInt(svData.imageDate.substring(5,7))
  503. }
  504. else{
  505. panoYear=parseInt(extractDate(coord.tags).year)
  506. panoMonth=parseInt(extractDate(coord.tags).month)
  507. }
  508.  
  509. let meta=getMetaData(svData)
  510. let yearTag=meta[0]
  511. let monthTag=parseInt(meta[1])
  512. let typeTag=meta[2]
  513. let subdivisionTag=meta[3]
  514. let localityTag=meta[4]
  515. let countryTag,elevationTag
  516. let genTag,trekkerTag,floorTag
  517. let dayTag,timeTag,exactTime,timeRange
  518.  
  519. if(monthTag){monthTag=months[monthTag-1]}
  520. if (!monthTag){monthTag='Month not found'}
  521.  
  522. var date=monthToTimestamp(svData.imageDate)
  523.  
  524. if(tags.includes('day')||tags.includes('time')){
  525. const initialSearch=await UE('SingleImageSearch',{'lat':coord.location.lat,'lng':coord.location.lng},date.startDate,date.endDate)
  526. if (initialSearch){
  527. if (initialSearch.length!=3)exactTime=null;
  528. else{
  529. exactTime=await binarySearch({'lat':coord.location.lat,'lng':coord.location.lng}, date.startDate,date.endDate)
  530. }
  531. }
  532.  
  533.  
  534. }
  535.  
  536. if(!exactTime){dayTag='Day not found'
  537. timeTag='Time not found'
  538. }
  539. else{
  540.  
  541. const currentDate = new Date();
  542. const currentOffset =-(currentDate.getTimezoneOffset())*60
  543. const dayOffset = currentOffset-Math.round((coord.location.lng / 15) * 3600);
  544. const LocalDay=new Date(Math.round(exactTime-dayOffset)*1000)
  545. dayTag = LocalDay.toISOString().split('T')[0];
  546.  
  547. if(tags.includes('time')) {
  548.  
  549. var localTime=await getLocal([coord.location.lat,coord.location.lng],exactTime)
  550. var timeObject=new Date(localTime*1000)
  551. timeTag =`${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`;
  552. var hour = timeObject.getHours();
  553.  
  554. if (hour < 11) {
  555. timeRange = 'Morning';
  556. } else if (hour >= 11 && hour < 13) {
  557. timeRange = 'Noon';
  558. } else if (hour >= 13 && hour < 17) {
  559. timeRange = 'Afternoon';
  560. } else if(hour >= 17 && hour < 19) {
  561. timeRange = 'Evening';
  562. }
  563. else{
  564. timeRange = 'Night';
  565. }
  566. }
  567. }
  568.  
  569. try {if (ccData.length!=3) ccData=ccData[1][0]
  570. else ccData=ccData[1]
  571. }
  572.  
  573. catch (error) {
  574. ccData=null
  575. }
  576.  
  577. if (ccData){
  578. try{
  579. countryTag = ccData[5][0][1][4]}
  580. catch(error){
  581. countryTag=null
  582. }
  583. try{
  584. elevationTag=ccData[5][0][1][1][0]}
  585. catch(error){
  586. elevationTag=null
  587. }
  588. try{
  589. trekkerTag=ccData[6][5]}
  590. catch(error){
  591. trekkerTag=null
  592. }
  593. try{
  594. floorTag=ccData[5][0][1][3][2][0]
  595. }
  596. catch(error){
  597. floorTag=null
  598. }
  599. if (tags.includes('detect')){
  600. const defaultDate=3
  601. }
  602. }
  603.  
  604. if (trekkerTag){
  605. trekkerTag=trekkerTag.toString()
  606. if( trekkerTag.includes('scout')){
  607. trekkerTag='trekker'
  608. }}
  609.  
  610. if(elevationTag){
  611. elevationTag=Math.round(elevationTag*100)/100
  612. if(sR){
  613. elevationTag=findRange(elevationTag,sR)
  614. }
  615. else{
  616. elevationTag=elevationTag.toString()+'m'
  617. }
  618. }
  619.  
  620. if (!countryTag)countryTag='Country not found'
  621. if (!elevationTag)elevationTag='Elevation not found'
  622.  
  623. if (tags.includes('generation')&&typeTag=='Official'&&countryTag){
  624. genTag = getGeneration(svData,countryTag)
  625. coord.tags.push(genTag)}
  626.  
  627. if (tags.includes('year'))coord.tags.push(yearTag)
  628.  
  629. if (tags.includes('month'))coord.tags.push(monthTag)
  630.  
  631. if (tags.includes('day'))coord.tags.push(dayTag)
  632.  
  633. if (tags.includes('time')) coord.tags.push(timeTag)
  634.  
  635. if (tags.includes('time')&&timeRange) coord.tags.push(timeRange)
  636.  
  637. if (tags.includes('type'))coord.tags.push(typeTag)
  638.  
  639. if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.tags.push('trekker')
  640.  
  641. if (tags.includes('type')&&floorTag&&typeTag=='Official')coord.tags.push(floorTag)
  642.  
  643. if (tags.includes('country'))coord.tags.push(countryTag)
  644.  
  645. if (tags.includes('subdivision')&&typeTag=='Official')coord.tags.push(subdivisionTag)
  646.  
  647. if (tags.includes('locality')&&typeTag=='Official')coord.tags.push(localityTag)
  648.  
  649. if (tags.includes('elevation'))coord.tags.push(elevationTag)
  650.  
  651. if (tags.includes('fix')){
  652. if (!fixStrategy){
  653. try{
  654. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,10)
  655. const updatedPnaoId=resultPano[1][1][1]
  656. const updatedYear=resultPano[1][6][7][0]
  657. const updatedMonth=resultPano[1][6][7][1]
  658. if (coord.panoId){
  659. if (updatedPnaoId&&updatedPnaoId!=coord.panoId) {
  660. if(panoYear!=updatedYear||panoMonth!=updatedMonth){
  661. coord.panoId=updatedPnaoId
  662. coord.tags.push('Updated')}
  663. else{
  664. coord.tags.push('Copyright changed')
  665. }
  666. }}
  667. else{
  668. if (panoYear&&panoMonth&&updatedYear&&updatedMonth){
  669. if(panoYear!=updatedYear||panoMonth!=updatedMonth){
  670. coord.panoId=updatedPnaoId
  671. coord.tags.push('Updated')
  672. }
  673. }
  674. else{
  675. coord.tags.push('Failed to update')
  676. }
  677. }
  678. }
  679. catch (error){
  680. coord.tags.push('Failed to update')
  681. }
  682. }
  683. }
  684. }
  685. }
  686. catch (error) {
  687. if(!tags.includes('fix'))coord.tags.push('Pano not found');
  688. else{
  689. var fixState
  690. try{
  691. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,5)
  692. if(fixStrategy){
  693. const panos=resultPano[1][5][0][8]
  694. for(const pano of panos){
  695. if((pano[1][0]===panoYear&&pano[1][1]===panoMonth)){
  696. const panoIndex=pano[0]
  697. const fixedPanoId=resultPano[1][5][0][3][0][panoIndex][0][1]
  698. coord.panoId=fixedPanoId
  699. coord.location.lat=resultPano[1][5][0][1][0][2]
  700. coord.location.lng=resultPano[1][5][0][1][0][3]
  701. fixState=true
  702. }
  703. }
  704. }
  705. else{
  706. coord.panoId=resultPano[1][1][1]
  707. fixState=true
  708. }
  709.  
  710. }
  711. catch (error){
  712. fixState=null
  713. }
  714. if (!fixState)coord.tags.push('Failed to fix')
  715. else coord.tags.push('Fixed')
  716.  
  717. }
  718. }
  719.  
  720. if (coord.tags) {coord.tags=Array.from(new Set(coord.tags))}
  721. taggedLocs.push(coord);
  722. }
  723.  
  724. async function processChunk(chunk, tags) {
  725. var service = new google.maps.StreetViewService();
  726. var promises = chunk.map(async coord => {
  727. let panoId = coord.panoId;
  728. let latLng = {lat: coord.location.lat, lng: coord.location.lng};
  729. let svData;
  730. let ccData;
  731. if ((panoId || latLng)) {
  732. if(tags!=['country']&&tags!=['elevation']&&tags!=['detect']){
  733. svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50});}
  734. }
  735.  
  736. if (!panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  737. ccData = await UE('SingleImageSearch', latLng);
  738. }
  739.  
  740. else if (panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  741. ccData = await UE('GetMetadata', panoId);
  742. }
  743.  
  744. if (latLng && (tags.includes('detect'))) {
  745. var detectYear,detectMonth
  746. if (coord.panoDate){
  747. detectYear=parseInt(coord.panoDate.substring(0,4))
  748. detectMonth=parseInt(coord.panoDate.substring(5,7))
  749. }
  750. else{
  751. if(coord.panoId){
  752. const metaData=await getSVData(service,{pano: panoId})
  753. if (metaData){
  754. if(metaData.imageDate){
  755. detectYear=parseInt(metaData.imageDate.substring(0,4))
  756. detectMonth=parseInt(metaData.imageDate.substring(5,7))
  757. }
  758. }
  759. }
  760. }
  761. if (detectYear&&detectMonth){
  762. const metaData = await UE('SingleImageSearch', latLng,10);
  763. if (metaData){
  764. if(metaData.length>1){
  765. const defaultDate=metaData[1][6][7]
  766. if (defaultDate[0]===detectYear&&defaultDate[1]!=detectMonth){
  767. coord.tags.push('Dangerous')}
  768. }
  769. }
  770. }
  771. }
  772. if (tags!=['detect']){
  773. await processCoord(coord, tags, svData,ccData)}
  774. });
  775. await Promise.all(promises);
  776.  
  777. }
  778.  
  779. function getSVData(service, options) {
  780. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  781. resolve(data);
  782.  
  783. }));
  784. }
  785.  
  786. async function processData(tags) {
  787. let successText='The JSON data has been pasted to your clipboard!';
  788. try {
  789. const totalChunks = Math.ceil(selections.length / CHUNK_SIZE);
  790. let processedChunks = 0;
  791.  
  792. const swal = Swal.fire({
  793. title: 'Tagging',
  794. text: 'If you try to tag a large number of locs by exact time or elevation, it could take quite some time. Please wait...',
  795. allowOutsideClick: false,
  796. allowEscapeKey: false,
  797. showConfirmButton: false,
  798. icon:"info",
  799. didOpen: () => {
  800. Swal.showLoading();
  801. }
  802. });
  803.  
  804. for (let i = 0; i < selections.length; i += CHUNK_SIZE) {
  805. let chunk = selections.slice(i, i + CHUNK_SIZE);
  806. await processChunk(chunk, tags);
  807. processedChunks++;
  808.  
  809. const progress = Math.min((processedChunks / totalChunks) * 100, 100);
  810. Swal.update({
  811. html: `<div>${progress.toFixed(2)}% completed</div>
  812. <div class="swal2-progress">
  813. <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;">
  814. </div>
  815. </div>`
  816. });
  817. if(exportMode){
  818. updateSelection(chunk)
  819. successText='Tagging completed! Do you want to refresh the page?(The JSON data is also pasted to your clipboard)'
  820. }
  821. }
  822.  
  823. swal.close();
  824. var newJSON=[]
  825. taggedLocs.forEach((loc)=>{
  826. newJSON.push({lat:loc.location.lat,
  827. lng:loc.location.lng,
  828. heading:loc.heading,
  829. pitch: loc.pitch !== undefined && loc.pitch !== null ? loc.pitch : 90,
  830. zoom: loc.zoom !== undefined && loc.zoom !== null ? loc.zoom : 0,
  831. panoId:loc.panoId,
  832. extra:{tags:loc.tags}
  833. })
  834. })
  835. GM_setClipboard(JSON.stringify(newJSON))
  836. Swal.fire({
  837. title: 'Success!',
  838. text: successText,
  839. icon: 'success',
  840. showCancelButton: true,
  841. confirmButtonColor: '#3085d6',
  842. cancelButtonColor: '#d33',
  843. confirmButtonText: 'OK'
  844. }).then((result) => {
  845. if (result.isConfirmed) {
  846. if(exportMode){
  847. location.reload();}
  848. }
  849. });
  850. } catch (error) {
  851. swal.close();
  852. Swal.fire('Error Tagging!', '','error');
  853. console.error('Error processing JSON data:', error);
  854. }
  855. }
  856.  
  857. if(selections){
  858. if(selections.length>=1){processData(tags);}
  859. else{
  860. 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');}
  861. }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');}
  862. }
  863.  
  864. function createCheckbox(text, tags) {
  865. var label = document.createElement('label');
  866. var checkbox = document.createElement('input');
  867. checkbox.type = 'checkbox';
  868. checkbox.value = text;
  869. checkbox.name = 'tags';
  870. checkbox.id = tags;
  871. checkbox.style.marginLeft='2px'
  872. label.appendChild(checkbox);
  873. label.appendChild(document.createTextNode(text));
  874. buttonContainer.appendChild(label);
  875. return checkbox;
  876. }
  877. const elementContainer=document.getElementsByClassName('tool-block')[0];
  878. var mainButton = document.createElement('button');
  879. mainButton.textContent = 'Auto-Tag';
  880. mainButton.id='main-button';
  881. mainButton.style.position = 'fixed';
  882. mainButton.style.right = '20px';
  883. mainButton.style.bottom = '15px';
  884. mainButton.style.borderRadius = "18px";
  885. mainButton.style.fontSize = "15px";
  886. mainButton.style.padding = "10px 20px";
  887. mainButton.style.border = "none";
  888. mainButton.style.color = "white";
  889. mainButton.style.cursor = "pointer";
  890. mainButton.style.backgroundColor = "#4CAF50";
  891. mainButton.addEventListener('click', function () {
  892. if (buttonContainer.style.display === 'none') {
  893. buttonContainer.style.display = 'block';
  894. } else {
  895. buttonContainer.style.display = 'none';
  896. }
  897. });
  898. document.body.appendChild(mainButton);
  899.  
  900. var buttonContainer = document.createElement('div');
  901. buttonContainer.style.position = 'fixed';
  902. buttonContainer.id='button-container';
  903. buttonContainer.style.right = '20px';
  904. buttonContainer.style.bottom = '50px';
  905. buttonContainer.style.display = 'none';
  906. buttonContainer.style.fontSize = '14px';
  907. document.body.appendChild(buttonContainer);
  908.  
  909. var selectAllCheckbox = document.createElement('input');
  910. selectAllCheckbox.type = 'checkbox';
  911. selectAllCheckbox.id = 'selectAll';
  912. selectAllCheckbox.addEventListener('change', function () {
  913. var checkboxes = document.getElementsByName('tags');
  914. for (var i = 0; i < checkboxes.length; i++) {
  915. if (!['Fix', 'Detect'].includes(checkboxes[i].value)){
  916. checkboxes[i].checked = this.checked;
  917. }
  918. }
  919. });
  920. var selectAllLabel = document.createElement('label');
  921. selectAllLabel.textContent = 'Select All';
  922. selectAllLabel.htmlFor = 'selectAll';
  923.  
  924.  
  925.  
  926. var triggerButton = document.createElement('button');
  927. triggerButton.textContent = 'Start Tagging';
  928. triggerButton.addEventListener('click', function () {
  929. var checkboxes = document.getElementsByName('tags');
  930. var checkedTags = [];
  931. for (var i = 0; i < checkboxes.length; i++) {
  932. if (checkboxes[i].checked) {
  933. checkedTags.push(checkboxes[i].id);
  934. }
  935. }
  936. if (checkedTags.includes('elevation')) {
  937. Swal.fire({
  938. title: 'Set A Range For Elevation',
  939. text: 'If you select "Cancel", the script will return the exact elevation for each location.',
  940. icon: 'question',
  941. showCancelButton: true,
  942. showCloseButton: true,
  943. allowOutsideClick: false,
  944. confirmButtonColor: '#3085d6',
  945. cancelButtonColor: '#d33',
  946. confirmButtonText: 'Yes',
  947. cancelButtonText: 'Cancel'
  948. }).then((result) => {
  949. if (result.isConfirmed) {
  950. Swal.fire({
  951. title: 'Define Range for Each Segment',
  952. html: `
  953. <label> <br>Enter range for each segment, separated by commas</br></label>
  954. <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea>
  955. `,
  956. icon: 'question',
  957. showCancelButton: true,
  958. showCloseButton: true,
  959. allowOutsideClick: false,
  960. focusConfirm: false,
  961. preConfirm: () => {
  962. const segmentRangesInput = document.getElementById('segmentRanges').value.trim();
  963. if (!segmentRangesInput) {
  964. Swal.showValidationMessage('Please enter range for each segment');
  965. return false;
  966. }
  967. const segmentRanges = segmentRangesInput.split(',');
  968. const validatedRanges = segmentRanges.map(range => {
  969. const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/);
  970. if (matches) {
  971. const min = Number(matches[1]);
  972. const max = Number(matches[2]);
  973. return { min, max };
  974. } else {
  975. Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue');
  976. return false;
  977. }
  978. });
  979. return validatedRanges.filter(Boolean);
  980. },
  981. confirmButtonColor: '#3085d6',
  982. cancelButtonColor: '#d33',
  983. confirmButtonText: 'Yes',
  984. cancelButtonText: 'Cancel',
  985. inputValidator: (value) => {
  986. if (!value.trim()) {
  987. return 'Please enter range for each segment';
  988. }
  989. }
  990. }).then((result) => {
  991. if (result.isConfirmed) {
  992. runScript(checkedTags, result.value)
  993. } else {
  994. Swal.showValidationMessage('You canceled input');
  995. }
  996. });
  997. }
  998. else if (result.dismiss === Swal.DismissReason.cancel) {
  999. runScript(checkedTags)
  1000. }
  1001. });
  1002. }
  1003. else {
  1004. runScript(checkedTags)
  1005. }
  1006. })
  1007. buttonContainer.appendChild(triggerButton);
  1008. buttonContainer.appendChild(selectAllCheckbox);
  1009. buttonContainer.appendChild(selectAllLabel);
  1010. tagBox.forEach(tag => {
  1011. createCheckbox(tag, tag.toLowerCase());
  1012. });
  1013.  
  1014. })();