Geoguessr Map-Making Auto-Tag

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

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

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