Geoguessr Map-Making Auto-Tag

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

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

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