Geoguessr Map-Making Auto-Tag

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

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

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