Geoguessr Map-Making Auto-Tag

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

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

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