Geoguessr Map-Making Auto-Tag

Tag your street view by date, exactTime, address, generation, elevation

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

  1. // ==UserScript==
  2. // @name Geoguessr Map-Making Auto-Tag
  3. // @namespace https://greasyfork.org/users/1179204
  4. // @version 3.85.9
  5. // @description Tag your street view 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', 'Generation', 'Elevation','Type','Update','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'
  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. } else {
  363. subdivision = svData.location.description;
  364. }
  365. }
  366. return [year,month,panoType,subdivision]
  367. }
  368. else{
  369. return null
  370. }
  371. }
  372.  
  373. function extractDate(array) {
  374. var year,month
  375. array.forEach(element => {
  376. const yearRegex1 = /^(\d{2})-(\d{2})$/;
  377. const yearRegex2 = /^(\d{4})-(\d{2})$/;
  378. const yearRegex3=/^(\d{4})$/;
  379. const monthRegex1 = /^(\d{2})$/;
  380. const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i;
  381.  
  382. if (!year &&!month&& yearRegex1.test(element)) {
  383. const match = yearRegex1.exec(element);
  384. year = parseInt(match[1]) + 2000;
  385. month = parseInt(match[2]);
  386. }
  387.  
  388. if (!year && !month&& yearRegex2.test(element)) {
  389. const match = yearRegex2.exec(element);
  390. year = parseInt(match[1]);
  391. month = parseInt(match[2]);
  392. }
  393. if(!year&& yearRegex3.test(element)){
  394. const match = yearRegex3.test(element);
  395. year=parseInt(element)
  396. }
  397. if (!month && monthRegex1.test(element)) {
  398. month= parseInt(element);
  399. }
  400.  
  401. if (!month && monthRegex2.test(element)) {
  402. const months = {
  403. "January": 1, "February": 2, "March": 3, "April": 4,
  404. "May": 5, "June": 6, "July": 7, "August": 8,
  405. "September": 9, "October": 10, "November": 11, "December": 12
  406. };
  407. month = months[element];
  408. }
  409. });
  410. return {year,month}
  411. }
  412.  
  413. function getGeneration(svData,country) {
  414. if (svData&&svData.tiles) {
  415. if (svData.tiles.worldSize.height === 1664) { // Gen 1
  416. return 'Gen1';
  417. } else if (svData.tiles.worldSize.height === 6656) { // Gen 2 or 3
  418.  
  419. let lat;
  420. for (let key in svData.Sv) {
  421. lat = svData.Sv[key].lat;
  422. break;
  423. }
  424.  
  425. let date;
  426. if (svData.imageDate) {
  427. date = new Date(svData.imageDate);
  428. } else {
  429. date = 'nodata';
  430. }
  431.  
  432. if (date!=='nodata'&&((country === 'BD' && (date >= new Date('2021-04'))) ||
  433. (country === 'EC' && (date >= new Date('2022-03'))) ||
  434. (country === 'FI' && (date >= new Date('2020-09'))) ||
  435. (country === 'IN' && (date >= new Date('2021-10'))) ||
  436. (country === 'LK' && (date >= new Date('2021-02'))) ||
  437. (country === 'KH' && (date >= new Date('2022-10'))) ||
  438. (country === 'LB' && (date >= new Date('2021-05'))) ||
  439. (country === 'NG' && (date >= new Date('2021-06'))) ||
  440. (country === 'ST') ||
  441. (country === 'US' && lat > 52 && (date >= new Date('2019-01'))))) {
  442. return 'Shitcam';
  443. }
  444.  
  445. let gen2Countries = ['AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK', 'GR', 'RO',
  446. 'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO', 'MC', 'SM',
  447. 'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT'];
  448. if (gen2Countries.includes(country)) {
  449.  
  450. return 'Gen2or3';
  451. }
  452. else{
  453. return 'Gen3';}
  454. }
  455. else if(svData.tiles.worldSize.height === 8192){
  456. return 'Gen4';
  457. }
  458. }
  459. return 'Unknown';
  460. }
  461.  
  462. async function getLocal(coord, timestamp) {
  463. const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
  464. try {
  465. const [lat, lng] = coord;
  466. const url = `https://api.wheretheiss.at/v1/coordinates/${lat},${lng}`;
  467.  
  468. const response = await fetch(url);
  469. if (!response.ok) {
  470. throw new Error("Request failed: " + response.statusText);
  471. }
  472. const data = await response.json();
  473. const targetTimezoneOffset = data.offset * 3600;
  474. const offsetDiff = systemTimezoneOffset - targetTimezoneOffset;
  475. const convertedTimestamp = Math.round(timestamp - offsetDiff);
  476. return convertedTimestamp;
  477. } catch (error) {
  478. throw error;
  479. }
  480. }
  481.  
  482. var CHUNK_SIZE = 1200;
  483. if (tags.includes('time')){
  484. CHUNK_SIZE = 500
  485. }
  486. var promises = [];
  487.  
  488. async function processCoord(coord, tags, svData,ccData) {
  489.  
  490. try{
  491. if (svData||ccData){
  492.  
  493. var panoYear,panoMonth
  494. if (coord.panoDate){
  495. panoYear=parseInt(coord.panoDate.substring(0,4))
  496. panoMonth=parseInt(coord.panoDate.substring(5,7))
  497. }
  498. else if(coord.panoId){
  499. panoYear=parseInt(svData.imageDate.substring(0,4))
  500. panoMonth=parseInt(svData.imageDate.substring(5,7))
  501. }
  502. else{
  503. panoYear=parseInt(extractDate(coord.tags).year)
  504. panoMonth=parseInt(extractDate(coord.tags).month)
  505. }
  506.  
  507. let meta=getMetaData(svData)
  508. let yearTag=meta[0]
  509. let monthTag=parseInt(meta[1])
  510. let typeTag=meta[2]
  511. let subdivisionTag=meta[3]
  512. let countryTag,elevationTag
  513. let genTag,trekkerTag,floorTag
  514. let dayTag,timeTag,exactTime,timeRange
  515.  
  516. if(monthTag){monthTag=months[monthTag-1]}
  517. if (!monthTag){monthTag='Month not found'}
  518.  
  519. var date=monthToTimestamp(svData.imageDate)
  520.  
  521. if(tags.includes('day')||tags.includes('time')){
  522. const initialSearch=await UE('SingleImageSearch',{'lat':coord.location.lat,'lng':coord.location.lng},date.startDate,date.endDate)
  523. if (initialSearch){
  524. if (initialSearch.length!=3)exactTime=null;
  525. else{
  526. exactTime=await binarySearch({'lat':coord.location.lat,'lng':coord.location.lng}, date.startDate,date.endDate)
  527. }
  528. }
  529.  
  530.  
  531. }
  532.  
  533. if(!exactTime){dayTag='Day not found'
  534. timeTag='Time not found'
  535. }
  536. else{
  537.  
  538. const currentDate = new Date();
  539. const currentOffset =-(currentDate.getTimezoneOffset())*60
  540. const dayOffset = currentOffset-Math.round((coord.location.lng / 15) * 3600);
  541. const LocalDay=new Date(Math.round(exactTime-dayOffset)*1000)
  542. dayTag = LocalDay.toISOString().split('T')[0];
  543.  
  544. if(tags.includes('time')) {
  545.  
  546. var localTime=await getLocal([coord.location.lat,coord.location.lng],exactTime)
  547. var timeObject=new Date(localTime*1000)
  548. timeTag =`${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`;
  549. var hour = timeObject.getHours();
  550.  
  551. if (hour < 11) {
  552. timeRange = 'Morning';
  553. } else if (hour >= 11 && hour < 13) {
  554. timeRange = 'Noon';
  555. } else if (hour >= 13 && hour < 17) {
  556. timeRange = 'Afternoon';
  557. } else if(hour >= 17 && hour < 19) {
  558. timeRange = 'Evening';
  559. }
  560. else{
  561. timeRange = 'Night';
  562. }
  563. }
  564. }
  565.  
  566. try {if (ccData.length!=3) ccData=ccData[1][0]
  567. else ccData=ccData[1]
  568. }
  569.  
  570. catch (error) {
  571. ccData=null
  572. }
  573.  
  574. if (ccData){
  575. try{
  576. countryTag = ccData[5][0][1][4]}
  577. catch(error){
  578. countryTag=null
  579. }
  580. try{
  581. elevationTag=ccData[5][0][1][1][0]}
  582. catch(error){
  583. elevationTag=null
  584. }
  585. try{
  586. trekkerTag=ccData[6][5]}
  587. catch(error){
  588. trekkerTag=null
  589. }
  590. try{
  591. floorTag=ccData[5][0][1][3][2][0]
  592. }
  593. catch(error){
  594. floorTag=null
  595. }
  596. if (tags.includes('detect')){
  597. const defaultDate=3
  598. }
  599. }
  600.  
  601. if (trekkerTag){
  602. trekkerTag=trekkerTag.toString()
  603. if( trekkerTag.includes('scout')){
  604. trekkerTag='trekker'
  605. }}
  606.  
  607. if(elevationTag){
  608. elevationTag=Math.round(elevationTag*100)/100
  609. if(sR){
  610. elevationTag=findRange(elevationTag,sR)
  611. }
  612. else{
  613. elevationTag=elevationTag.toString()+'m'
  614. }
  615. }
  616.  
  617. if (!countryTag)countryTag='Country not found'
  618. if (!elevationTag)elevationTag='Elevation not found'
  619.  
  620. if (tags.includes('generation')&&typeTag=='Official'&&countryTag){
  621. genTag = getGeneration(svData,countryTag)
  622. coord.tags.push(genTag)}
  623.  
  624. if (tags.includes('year'))coord.tags.push(yearTag)
  625.  
  626. if (tags.includes('month'))coord.tags.push(monthTag)
  627.  
  628. if (tags.includes('day'))coord.tags.push(dayTag)
  629.  
  630. if (tags.includes('time')) coord.tags.push(timeTag)
  631.  
  632. if (tags.includes('time')&&timeRange) coord.tags.push(timeRange)
  633.  
  634. if (tags.includes('type'))coord.tags.push(typeTag)
  635.  
  636. if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.tags.push('trekker')
  637.  
  638. if (tags.includes('type')&&floorTag&&typeTag=='Official')coord.tags.push(floorTag)
  639.  
  640. if (tags.includes('country'))coord.tags.push(countryTag)
  641.  
  642. if (tags.includes('subdivision')&&typeTag=='Official')coord.tags.push(subdivisionTag)
  643.  
  644. if (tags.includes('elevation'))coord.tags.push(elevationTag)
  645.  
  646. if (tags.includes('update')){
  647. try{
  648. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,10)
  649. const updatedPnaoId=resultPano[1][1][1]
  650. const updatedYear=resultPano[1][6][7][0]
  651. const updatedMonth=resultPano[1][6][7][1]
  652. if (coord.panoId){
  653. if (updatedPnaoId&&updatedPnaoId!=coord.panoId) {
  654. if(panoYear!=updatedYear||panoMonth!=updatedMonth){
  655. coord.panoId=updatedPnaoId
  656. coord.tags.push('Updated')}
  657. else{
  658. coord.tags.push('Copyright changed')
  659. }
  660. }}
  661. else{
  662. if (panoYear&&panoMonth&&updatedYear&&updatedMonth){
  663. if(panoYear!=updatedYear||panoMonth!=updatedMonth){
  664. coord.panoId=updatedPnaoId
  665. coord.tags.push('Updated')
  666. }
  667. }
  668. else{
  669. coord.tags.push('Failed to update')
  670. }
  671. }
  672. }
  673. catch (error){
  674. coord.tags.push('Failed to update')
  675. }
  676. }
  677. }
  678. }
  679. catch (error) {
  680. if(!tags.includes('fix'))coord.tags.push('Pano not found');
  681. else{
  682. var fixState
  683. try{
  684. const resultPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,5)
  685. if(fixStrategy){
  686. const panos=resultPano[1][5][0][8]
  687. for(const pano of panos){
  688. if((pano[1][0]===panoYear&&pano[1][1]===panoMonth)){
  689. const panoIndex=pano[0]
  690. const fixedPanoId=resultPano[1][5][0][3][0][panoIndex][0][1]
  691. coord.panoId=fixedPanoId
  692. coord.location.lat=resultPano[1][5][0][1][0][2]
  693. coord.location.lng=resultPano[1][5][0][1][0][3]
  694. fixState=true
  695. }
  696. }
  697. }
  698. else{
  699. coord.panoId=resultPano[1][1][1]
  700. fixState=true
  701. }
  702.  
  703. }
  704. catch (error){
  705. fixState=null
  706. }
  707. if (!fixState)coord.tags.push('Failed to fix')
  708. else coord.tags.push('Fixed')
  709.  
  710. }
  711. }
  712.  
  713. if (coord.tags) {coord.tags=Array.from(new Set(coord.tags))}
  714. taggedLocs.push(coord);
  715. }
  716.  
  717. async function processChunk(chunk, tags) {
  718. var service = new google.maps.StreetViewService();
  719. var promises = chunk.map(async coord => {
  720. let panoId = coord.panoId;
  721. let latLng = {lat: coord.location.lat, lng: coord.location.lng};
  722. let svData;
  723. let ccData;
  724. if ((panoId || latLng)) {
  725. if(tags!=['country']&&tags!=['elevation']&&tags!=['detect']){
  726. svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50});}
  727. }
  728.  
  729. if (!panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  730. ccData = await UE('SingleImageSearch', latLng);
  731. }
  732.  
  733. else if (panoId && (tags.includes('generation')||('country')||('elevation')||('type'))) {
  734. ccData = await UE('GetMetadata', panoId);
  735. }
  736.  
  737. if (latLng && (tags.includes('detect'))) {
  738. var detectYear,detectMonth
  739. if (coord.panoDate){
  740. detectYear=parseInt(coord.panoDate.substring(0,4))
  741. detectMonth=parseInt(coord.panoDate.substring(5,7))
  742. }
  743. else{
  744. if(coord.panoId){
  745. const metaData=await getSVData(service,{pano: panoId})
  746. if (metaData){
  747. if(metaData.imageDate){
  748. detectYear=parseInt(metaData.imageDate.substring(0,4))
  749. detectMonth=parseInt(metaData.imageDate.substring(5,7))
  750. }
  751. }
  752. }
  753. }
  754. if (detectYear&&detectMonth){
  755. const metaData = await UE('SingleImageSearch', latLng,10);
  756. if (metaData){
  757. if(metaData.length>1){
  758. const defaultDate=metaData[1][6][7]
  759. if (defaultDate[0]===detectYear&&defaultDate[1]!=detectMonth){
  760. coord.tags.push('Dangerous')}
  761. }
  762. }
  763. }
  764. }
  765. if (tags!=['detect']){
  766. await processCoord(coord, tags, svData,ccData)}
  767. });
  768. await Promise.all(promises);
  769.  
  770. }
  771.  
  772. function getSVData(service, options) {
  773. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  774. resolve(data);
  775.  
  776. }));
  777. }
  778.  
  779. async function processData(tags) {
  780. let successText='The JSON data has been pasted to your clipboard!';
  781. try {
  782. const totalChunks = Math.ceil(selections.length / CHUNK_SIZE);
  783. let processedChunks = 0;
  784.  
  785. const swal = Swal.fire({
  786. title: 'Tagging',
  787. text: 'If you try to tag a large number of locs by exact time or elevation, it could take quite some time. Please wait...',
  788. allowOutsideClick: false,
  789. allowEscapeKey: false,
  790. showConfirmButton: false,
  791. icon:"info",
  792. didOpen: () => {
  793. Swal.showLoading();
  794. }
  795. });
  796.  
  797. for (let i = 0; i < selections.length; i += CHUNK_SIZE) {
  798. let chunk = selections.slice(i, i + CHUNK_SIZE);
  799. await processChunk(chunk, tags);
  800. processedChunks++;
  801.  
  802. const progress = Math.min((processedChunks / totalChunks) * 100, 100);
  803. Swal.update({
  804. html: `<div>${progress.toFixed(2)}% completed</div>
  805. <div class="swal2-progress">
  806. <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;">
  807. </div>
  808. </div>`
  809. });
  810. if(exportMode){
  811. updateSelection(chunk)
  812. successText='Tagging completed! Do you want to refresh the page?(The JSON data is also pasted to your clipboard)'
  813. }
  814. }
  815.  
  816. swal.close();
  817. var newJSON=[]
  818. taggedLocs.forEach((loc)=>{
  819. newJSON.push({lat:loc.location.lat,
  820. lng:loc.location.lng,
  821. heading:loc.heading,
  822. pitch: loc.pitch !== undefined && loc.pitch !== null ? loc.pitch : 90,
  823. zoom: loc.zoom !== undefined && loc.zoom !== null ? loc.zoom : 0,
  824. panoId:loc.panoId,
  825. extra:{tags:loc.tags}
  826. })
  827. })
  828. GM_setClipboard(JSON.stringify(newJSON))
  829. Swal.fire({
  830. title: 'Success!',
  831. text: successText,
  832. icon: 'success',
  833. showCancelButton: true,
  834. confirmButtonColor: '#3085d6',
  835. cancelButtonColor: '#d33',
  836. confirmButtonText: 'OK'
  837. }).then((result) => {
  838. if (result.isConfirmed) {
  839. if(exportMode){
  840. location.reload();}
  841. }
  842. });
  843. } catch (error) {
  844. swal.close();
  845. Swal.fire('Error Tagging!', '','error');
  846. console.error('Error processing JSON data:', error);
  847. }
  848. }
  849.  
  850. if(selections){
  851. if(selections.length>=1){processData(tags);}
  852. else{
  853. 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');}
  854. }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');}
  855. }
  856.  
  857. function createCheckbox(text, tags) {
  858. var label = document.createElement('label');
  859. var checkbox = document.createElement('input');
  860. checkbox.type = 'checkbox';
  861. checkbox.value = text;
  862. checkbox.name = 'tags';
  863. checkbox.id = tags;
  864. checkbox.style.marginLeft='2px'
  865. label.appendChild(checkbox);
  866. label.appendChild(document.createTextNode(text));
  867. buttonContainer.appendChild(label);
  868. return checkbox;
  869. }
  870. const elementContainer=document.getElementsByClassName('tool-block')[0];
  871. var mainButton = document.createElement('button');
  872. mainButton.textContent = 'Auto-Tag';
  873. mainButton.id='main-button';
  874. mainButton.style.position = 'fixed';
  875. mainButton.style.right = '20px';
  876. mainButton.style.bottom = '15px';
  877. mainButton.style.borderRadius = "18px";
  878. mainButton.style.fontSize = "15px";
  879. mainButton.style.padding = "10px 20px";
  880. mainButton.style.border = "none";
  881. mainButton.style.color = "white";
  882. mainButton.style.cursor = "pointer";
  883. mainButton.style.backgroundColor = "#4CAF50";
  884. mainButton.addEventListener('click', function () {
  885. if (buttonContainer.style.display === 'none') {
  886. buttonContainer.style.display = 'block';
  887. } else {
  888. buttonContainer.style.display = 'none';
  889. }
  890. });
  891. document.body.appendChild(mainButton);
  892.  
  893. var buttonContainer = document.createElement('div');
  894. buttonContainer.style.position = 'fixed';
  895. buttonContainer.id='button-container';
  896. buttonContainer.style.right = '20px';
  897. buttonContainer.style.bottom = '50px';
  898. buttonContainer.style.display = 'none';
  899. buttonContainer.style.fontSize = '14px';
  900. document.body.appendChild(buttonContainer);
  901.  
  902. var selectAllCheckbox = document.createElement('input');
  903. selectAllCheckbox.type = 'checkbox';
  904. selectAllCheckbox.id = 'selectAll';
  905. selectAllCheckbox.addEventListener('change', function () {
  906. var checkboxes = document.getElementsByName('tags');
  907. for (var i = 0; i < checkboxes.length; i++) {
  908. if (!['Fix', 'Detect'].includes(checkboxes[i].value)){
  909. checkboxes[i].checked = this.checked;
  910. }
  911. }
  912. });
  913. var selectAllLabel = document.createElement('label');
  914. selectAllLabel.textContent = 'Select All';
  915. selectAllLabel.htmlFor = 'selectAll';
  916.  
  917.  
  918.  
  919. var triggerButton = document.createElement('button');
  920. triggerButton.textContent = 'Start Tagging';
  921. triggerButton.addEventListener('click', function () {
  922. var checkboxes = document.getElementsByName('tags');
  923. var checkedTags = [];
  924. for (var i = 0; i < checkboxes.length; i++) {
  925. if (checkboxes[i].checked) {
  926. checkedTags.push(checkboxes[i].id);
  927. }
  928. }
  929. if (checkedTags.includes('elevation')) {
  930. Swal.fire({
  931. title: 'Set A Range For Elevation',
  932. text: 'If you select "Cancel", the script will return the exact elevation for each location.',
  933. icon: 'question',
  934. showCancelButton: true,
  935. showCloseButton: true,
  936. allowOutsideClick: false,
  937. confirmButtonColor: '#3085d6',
  938. cancelButtonColor: '#d33',
  939. confirmButtonText: 'Yes',
  940. cancelButtonText: 'Cancel'
  941. }).then((result) => {
  942. if (result.isConfirmed) {
  943. Swal.fire({
  944. title: 'Define Range for Each Segment',
  945. html: `
  946. <label> <br>Enter range for each segment, separated by commas</br></label>
  947. <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea>
  948. `,
  949. icon: 'question',
  950. showCancelButton: true,
  951. showCloseButton: true,
  952. allowOutsideClick: false,
  953. focusConfirm: false,
  954. preConfirm: () => {
  955. const segmentRangesInput = document.getElementById('segmentRanges').value.trim();
  956. if (!segmentRangesInput) {
  957. Swal.showValidationMessage('Please enter range for each segment');
  958. return false;
  959. }
  960. const segmentRanges = segmentRangesInput.split(',');
  961. const validatedRanges = segmentRanges.map(range => {
  962. const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/);
  963. if (matches) {
  964. const min = Number(matches[1]);
  965. const max = Number(matches[2]);
  966. return { min, max };
  967. } else {
  968. Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue');
  969. return false;
  970. }
  971. });
  972. return validatedRanges.filter(Boolean);
  973. },
  974. confirmButtonColor: '#3085d6',
  975. cancelButtonColor: '#d33',
  976. confirmButtonText: 'Yes',
  977. cancelButtonText: 'Cancel',
  978. inputValidator: (value) => {
  979. if (!value.trim()) {
  980. return 'Please enter range for each segment';
  981. }
  982. }
  983. }).then((result) => {
  984. if (result.isConfirmed) {
  985. runScript(checkedTags, result.value)
  986. } else {
  987. Swal.showValidationMessage('You canceled input');
  988. }
  989. });
  990. }
  991. else if (result.dismiss === Swal.DismissReason.cancel) {
  992. runScript(checkedTags)
  993. }
  994. });
  995. }
  996. else {
  997. runScript(checkedTags)
  998. }
  999. })
  1000. buttonContainer.appendChild(triggerButton);
  1001. buttonContainer.appendChild(selectAllCheckbox);
  1002. buttonContainer.appendChild(selectAllLabel);
  1003. tagBox.forEach(tag => {
  1004. createCheckbox(tag, tag.toLowerCase());
  1005. });
  1006.  
  1007. })();