Geoguessr Map-Making Auto-Tag

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

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

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