Geoguessr Map-Making Auto-Tag

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

当前为 2024-06-20 提交的版本,查看 最新版本

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