Geoguessr Map-Making Auto-Tag

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

当前为 2025-01-11 提交的版本,查看 最新版本

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