Geoguessr Map-Making Auto-Tag

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

目前為 2024-08-20 提交的版本,檢視 最新版本

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