Geoguessr Map-Making Auto-Tag

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

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