Tag your panos by date, exactTime, address, generation, elevation
// ==UserScript== // @name Geoguessr Map-Making Auto-Tag // @namespace https://greasyfork.org/users/1179204 // @version 3.89.3 // @description Tag your panos by date, exactTime, address, generation, elevation // @author KaKa // @match *://map-making.app/maps/* // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @require https://cdn.jsdelivr.net/npm/[email protected]/suncalc.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/geotz.min.js // @license BSD // @icon https://www.svgrepo.com/show/423677/tag-price-label.svg // ==/UserScript== (function() { 'use strict'; let accuracy=60 /* You could modifiy accuracy here, default setting is 60s */ let tagBox = ['Year', 'Month','Day', 'Time','Sun','Weather','Type','Country', 'Subdivision', 'Road','Generation', 'Elevation','Brightness','Driving Direction','Pan to Sun','Reset Heading','Update','Fix'] let months = ['January', 'February', 'March', 'April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December']; let tooltips = { 'Year': 'Year of pano in format yyyy', 'Month': 'Month of pano in format yy-mm', 'Day': 'Specific date of pano in format yyyy-mm-dd', 'Time': 'Exact time of pano with optional time range description, e.g., 09:35:21 marked as Morning', 'Country': 'Country of pano (Google data)', 'Subdivision': 'Primary administrative subdivision of pano location', 'Road': 'Road name of pano location', 'Generation': 'Camera generation of pano, categorized as Gen1, Gen2orGen3, Gen3, Gen4, Shitcam', 'Elevation': 'Elevation of street view location (Google data)', 'Brightness':'Average brightness of pano', 'Type': 'Type of pano, categorized as Official, Unofficial, Trekker/Tripod', 'Driving Direction': 'Absolute driving direction of streetview vehicle', 'Reset Heading': 'Reset heading to default driving direction', 'Fix': 'Fix broken locs by updating to latest coverage or searching for specific coverage based on saved date from map-making', 'Update': 'Update pano to latest coverage or based on saved date from map-making, effective only for locs with panoID', 'Detect': "Detect pano that is about to be removed and mark it as 'Dangerous' ", 'Sun':'Detect whether it is sunset or sunrise coverage(effective only for defalut coverage)', 'Pan to Sun':'Make pano heading to sun(moon),effective only for defalut coverage', 'Weather':'Weather type recorded by the closest weather station closest, with an accuracy of 10mins(effective only for defalut coverage)' }; const weatherCodeMap = { 0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Mostly cloudy', 4: 'Overcast', 61:'Slight Rain', 63:'Moderate Rain', 65:'Heavy Rain', 51:'Light Drizzle', 53:'Moderate Drizzle', 55:'Dense Drizzle', 77:'Snow', 85:'Slight Snow', 86:'Heavy Snow', }; function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } const datePattern = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([.,]\d{1,3})?Z?)$/; if (Array.isArray(obj)) { return obj.map(item => deepClone(item)); } if (obj instanceof Date) { return new Date(obj.getTime()); } if (typeof obj === 'object') { const clonedObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'string' && datePattern.test(obj[key])) { clonedObj[key] = new Date(obj[key]); } else { clonedObj[key] = deepClone(obj[key]); } } } return clonedObj; } return obj; } function getSelection() { const editor = unsafeWindow.editor; if (editor) { const selectedLocs = editor.selections; const selections = deepClone( selectedLocs.flatMap(selection => selection.locations) ); return selections; } } function updateLocation(o,n) { const editor=unsafeWindow.editor if (editor){ editor.removeLocations(o) editor.importLocations(n) } } function findRange(elevation, ranges) { for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; if (elevation >= range.min && elevation <= range.max) { return `${range.min}-${range.max}m`; } } if (!elevation) { return 'noElevation'; } return `${JSON.stringify(elevation)}m`; } async function runScript(tags,sR) { let taggedLocs=[] let exportMode,selections,fixStrategy if (tags.length<1){ swal.fire('Feature not found!', 'Please select at least one feature!','warning') return} if (tags.includes('fix')){ const { value: fixOption,dismiss: fixDismiss } = await Swal.fire({ title:'Fix Strategy', icon:'question', 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.', showCancelButton: true, showCloseButton:true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'No', }) if(fixOption)fixStrategy='exactly' else if(!fixOption&&fixDismiss==='cancel'){ fixStrategy=null } else{ return } }; const { value: option, dismiss: inputDismiss } = await Swal.fire({ title: 'Export', 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.', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel' }); if (option) { exportMode = 'save' } else if (!selections && inputDismiss === 'cancel') { exportMode = false } else { return } selections=getSelection() if (!selections||selections.length<1){ swal.fire('Selection not found!', 'Please select at least one location as selection!','warning') return } var CHUNK_SIZE = 1200; if (tags.includes('time')){ CHUNK_SIZE = 1000 } var promises = []; if(selections){ if(selections.length>=1){processData(tags);} else{ 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');} }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');} async function UE(t, e, s, d,r) { try { const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`; let payload = createPayload(t, e,s,d,r); const response = await fetch(u, { method: "POST", headers: { "content-type": "application/json+protobuf", "x-user-agent": "grpc-web-javascript/0.1" }, body: payload, mode: "cors", credentials: "omit" }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return await response.json(); } } catch (error) { console.error(`There was a problem with the UE function: ${error.message}`); } } function createPayload(mode,coorData,s,d,r) { let payload; if(!r)r=50 // default search radius if (mode === 'GetMetadata') { payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]]; } else if (mode === 'SingleImageSearch') { if(s&&d){ 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]]] }else{ payload =[["apiv3"], [[null,null,coorData.lat,coorData.lng],r], [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,true,2]]]], [[1,2,3,4,8,6]]];} } else { throw new Error("Invalid mode!"); } return JSON.stringify(payload); } function monthToTimestamp(m) { const [year, month] = m.split('-'); const startDate =Math.round( new Date(year, month-1,1).getTime()/1000); const endDate =Math.round( new Date(year, month, 1).getTime()/1000)-1; return { startDate, endDate }; } async function binarySearch(c, start,end) { let capture let response while (end - start >= accuracy) { let mid= Math.round((start + end) / 2); response = await UE("SingleImageSearch", c, start,end,15); if (response&&response[0][2]== "Search returned no images." ){ start=mid+start-end end=start-mid+end mid=Math.round((start+end)/2) } else { start=mid mid=Math.round((start+end)/2) } capture=mid } return capture } function getMetaData(svData) { let year = 'Year not found',month = 'Month not found' let panoType='unofficial' let subdivision='Subdivision not found' let defaultHeading=null if (svData) { if (svData.imageDate) { const matchYear = svData.imageDate.match(/\d{4}/); if (matchYear) { year = matchYear[0]; } const matchMonth = svData.imageDate.match(/-(\d{2})/); if (matchMonth) { month = matchMonth[1]; } } if (svData.copyright.includes('Google')) { panoType = 'Official'; } if (svData.tiles&&svData.tiles&&svData.tiles.originHeading){ defaultHeading=svData.tiles.originHeading } if(svData.location.description){ let parts = svData.location.description.split(','); if(parts.length > 1){ subdivision = parts[parts.length-1].trim(); } else { subdivision = svData.location.description; } } return [year,month,panoType,subdivision,defaultHeading] } else{ return null } } function extractDate(array) { let year, month; array.forEach(element => { const yearRegex1 = /^(\d{2})-(\d{2})$/; // Matches yy-mm const yearRegex2 = /^(\d{4})-(\d{2})$/; // Matches yyyy-mm const yearRegex3 = /^(\d{4})$/; // Matches yyyy const monthRegex1 = /^(\d{2})$/; // Matches mm const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i; // Matches month names if (!month && yearRegex1.test(element)) { const match = yearRegex1.exec(element); year = parseInt(match[1]) + 2000; // Convert to full year month = parseInt(match[2]); } if (!month && yearRegex2.test(element)) { const match = yearRegex2.exec(element); year = parseInt(match[1]); month = parseInt(match[2]); } if (!year && yearRegex3.test(element)) { year = parseInt(element); } if (!month && monthRegex1.test(element)) { month = parseInt(element); } if (!month && monthRegex2.test(element)) { const months = { "January": 1, "February": 2, "March": 3, "April": 4, "May": 5, "June": 6, "July": 7, "August": 8, "September": 9, "October": 10, "November": 11, "December": 12 }; month = months[element]; } }); return {year,month} } function getDirection(heading) { if (typeof heading !== 'number' || heading < 0 || heading >= 360) { return 'Unknown direction'; } const directions = [ { name: 'North', range: [337.5, 22.5] }, { name: 'Northeast', range: [22.5, 67.5] }, { name: 'East', range: [67.5, 112.5] }, { name: 'Southeast', range: [112.5, 157.5] }, { name: 'South', range: [157.5, 202.5] }, { name: 'Southwest', range: [202.5, 247.5] }, { name: 'West', range: [247.5, 292.5] }, { name: 'Northwest', range: [292.5, 337.5] } ]; for (const direction of directions) { const [start, end] = direction.range; if (start <= end) { if (heading >= start && heading < end) { return direction.name; } } else { if (heading >= start || heading < end) { return direction.name; } } } return 'Unknown direction'; } function getGeneration(svData,country) { let gen2Countries = ['AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK', 'GR', 'RO', 'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO', 'MC', 'SM', 'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT', 'SJ']; if (svData&&svData.tiles) { if (svData.tiles.worldSize.height === 1664) { // Gen 1 return 'Gen1'; } else if (svData.tiles.worldSize.height === 6656) { // Gen 2 or 3 let lat; for (let key in svData.Sv) { lat = svData.Sv[key].lat; break; } let date; if (svData.imageDate) { date = new Date(svData.imageDate); } else { date = 'nodata'; } if (date!=='nodata'&&((country === 'BD' && (date >= new Date('2021-04'))) || (country === 'EC' && (date >= new Date('2022-03'))) || (country === 'FI' && (date >= new Date('2020-09'))) || (country === 'IN' && (date >= new Date('2021-10'))) || (country === 'LK' && (date >= new Date('2021-02'))) || (country === 'KH' && (date >= new Date('2022-10'))) || (country === 'LB' && (date >= new Date('2021-05'))) || (country === 'NG' && (date >= new Date('2021-06'))) || (country === 'ST') || (country === 'US' && lat > 52 && (date >= new Date('2019-01'))))) { return 'Shitcam'; } if (gen2Countries.includes(country)||country=='Country not found'||!country) { return 'Gen2or3'; } else{ return 'Gen3';} } else if(svData.tiles.worldSize.height === 8192){ return 'Gen4'; } } return 'Unknown'; } async function getLocal(coord, timestamp) { const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60; try { var offset_hours const timezone=await GeoTZ.find(coord[0],coord[1]) const offset = await GeoTZ.toOffset(timezone); if(offset){ offset_hours=parseInt(offset/60) } else if (offset===0) offset_hours=0 const offsetDiff = systemTimezoneOffset -offset_hours*3600; const convertedTimestamp = Math.round(timestamp - offsetDiff); return convertedTimestamp; } catch (error) { throw error; } } async function getWeather(coordinate, timestamp) { var hours,weatherCodes const date = new Date(timestamp * 1000); const formatted_date = date.toISOString().split('T')[0] try { 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`; const response = await fetch(url); const data = await response.json(); hours = data.hourly.time; weatherCodes = data.hourly.weather_code; const targetHour = new Date(timestamp * 1000).getHours(); let closestHourIndex = 0; let minDiff = Infinity; for (let i = 0; i < hours.length; i++) { const hour = new Date(hours[i]).getHours(); const diff = Math.abs(hour - targetHour); if (diff < minDiff) { minDiff = diff; closestHourIndex = i; } } const weatherCode = weatherCodes[closestHourIndex]; const weatherDescription = weatherCodeMap[weatherCode] || 'Unknown weather code'; return weatherDescription; } catch (error) { console.error('Error fetching weather data:', error); return 'Network request failed' } } async function processCoord(coord, tags, svData,ccData) { var panoYear,panoMonth if (tags.includes(('fix')||('update')||('detect'))){ if (coord.panoDate){ panoYear=parseInt(coord.panoDate.toISOString().substring(0,4)) panoMonth=parseInt(coord.panoDate.toISOString().substring(5,7)) } else if(svData&&svData.imageDate){ panoYear=parseInt(svData.imageDate.substring(0,4)) panoMonth=parseInt(svData.imageDate.substring(5,7)) } else{ panoYear=parseInt(extractDate(coord.tags).year) panoMonth=parseInt(extractDate(coord.tags).month) } } try{ let meta=getMetaData(svData) let yearTag=meta[0] let monthTag=parseInt(meta[1]) let typeTag=meta[2] let subdivisionTag=meta[3] let countryTag,elevationTag let genTag,trekkerTag,floorTag,driDirTag,weatherTag,roadTag let dayTag,timeTag,exactTime,timeRange //if(monthTag){monthTag=months[monthTag-1]} if(yearTag&&monthTag) monthTag=yearTag.slice(-2)+'-'+(monthTag.toString()) if (!monthTag){monthTag='Month not found'} var date=monthToTimestamp(svData.imageDate) if(tags.includes('day')||tags.includes('time')||tags.includes('sun')||tags.includes('weather')||tags.includes('pan to sun')){ const initialSearch=await UE('SingleImageSearch',{lat:coord.location.lat,lng:coord.location.lng},date.startDate,date.endDate,15) if (initialSearch){ if (initialSearch.length!=3)exactTime=null; else{ if(!tags.includes('day')) accuracy=18000 if(tags.includes('weather')) accuracy=600 if (tags.includes('sun')) accuracy=300 if (tags.includes('pan to sun')) accuracy=300 if (tags.includes('time')) accuracy=60 exactTime=await binarySearch({lat:coord.location.lat,lng:coord.location.lng}, date.startDate,date.endDate) } } } if(!exactTime){dayTag='Day not found' timeTag='Time not found'} else{ if (tags.includes('day')){ const currentDate = new Date(); const currentOffset =-(currentDate.getTimezoneOffset())*60 const dayOffset = currentOffset-Math.round((coord.location.lng / 15) * 3600); const LocalDay=new Date(Math.round(exactTime-dayOffset)*1000) dayTag = LocalDay.toISOString().split('T')[0]; } if(tags.includes('time')) { var localTime=await getLocal([coord.location.lat,coord.location.lng],exactTime) var timeObject=new Date(localTime*1000) timeTag =`${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`; var hour = timeObject.getHours(); if (hour < 11) { timeRange = 'Morning'; } else if (hour >= 11 && hour < 13) { timeRange = 'Noon'; } else if (hour >= 13 && hour < 17) { timeRange = 'Afternoon'; } else if(hour >= 17 && hour < 19) { timeRange = 'Dusk'; } else{ timeRange = 'Night'; } } if (tags.includes('sun')){ const utcDate=new Date(exactTime*1000) const sunData=calSun(utcDate.toISOString(),coord.location.lat,coord.location.lng) if(sunData){ if (exactTime>=(sunData.sunset-30*60)&&exactTime<=(sunData.sunset+30*60)){ coord.tags.push('Sunset') } else if (exactTime>=(sunData.sunset-90*60)&&exactTime<=(sunData.sunset+90*60)){ coord.tags.push('Sunset(check)') } else if (exactTime>=(sunData.sunrise-30*60)&&exactTime<=(sunData.sunrise+30*60)){ coord.tags.push('Sunrise') } else if (exactTime>=(sunData.sunrise-90*60)&&exactTime<=(sunData.sunrise+90*60)){ coord.tags.push('Sunrise(check)') } else if (exactTime>=(sunData.noon-30*60)&&exactTime<=(sunData.noon+30*60)){ coord.tags.push('Noon') } } } if (tags.includes('pan to sun')){ const date = new Date(exactTime * 1000); const position = SunCalc.getPosition(date, coord.location.lat, coord.location.lng); const altitude = position.altitude; const azimuth = position.azimuth; const altitudeDegrees = altitude * (180 / Math.PI); const azimuthDegrees = azimuth * (180 / Math.PI); if(azimuthDegrees&&altitudeDegrees){ if (altitudeDegrees<0){ const moonPosition = SunCalc.getMoonPosition(date, coord.location.lat, coord.location.lng); const moon_altitude = moonPosition.altitude; const moon_azimuth = moonPosition.azimuth; const moon_altitudeDegrees = moon_altitude * (180 / Math.PI); const moon_azimuthDegrees = moon_azimuth * (180 / Math.PI); coord.heading=moon_azimuthDegrees+180 coord.pitch=moon_altitudeDegrees coord.zoom=2 coord.tags.push('pan to moon') } else{ coord.heading=azimuthDegrees+180 coord.pitch=altitudeDegrees coord.tags.push('pan to sun') } } } if(tags.includes('weather')) { weatherTag=await getWeather(coord.location,exactTime) if(weatherTag) coord.tags.push(weatherTag) } } try {if (ccData.length!=3) ccData=ccData[1][0] else ccData=ccData[1] } catch (error) { ccData=null } if (ccData){ try{ countryTag = ccData[5][0][1][4]} catch(error){ countryTag=null } try{ elevationTag=ccData[5][0][1][1][0]} catch(error){ elevationTag=null } try{ roadTag=ccData[5][0][12][0][0][0][2][0]} catch(error){ roadTag=null } try{ driDirTag=ccData[5][0][1][2][0]} catch(error){ driDirTag=null } try{ trekkerTag=ccData[6][5]} catch(error){ trekkerTag=null } try{ floorTag=ccData[5][0][1][3][2][0] } catch(error){ floorTag=null } if (tags.includes('detect')){ const defaultDate=3 } } if (roadTag==''||!roadTag)roadTag='Road not found' if (trekkerTag){ trekkerTag=trekkerTag.toString() if( trekkerTag.includes('scout')&&floorTag){ trekkerTag='trekker' } else{ trekkerTag=false }} if(elevationTag){ elevationTag=Math.round(elevationTag*100)/100 if(sR){ elevationTag=findRange(elevationTag,sR) } else{ elevationTag=elevationTag.toString()+'m' } } if(driDirTag){ driDirTag=getDirection(parseFloat(driDirTag)) } else{ driDirTag='Driving direction not found' } if (!countryTag)countryTag='Country not found' if (!elevationTag)elevationTag='Elevation not found' if (tags.includes('generation')&&typeTag=='Official'&&countryTag){ genTag = getGeneration(svData,countryTag) coord.tags.push(genTag)} if (tags.includes('year'))coord.tags.push(yearTag) if (tags.includes('month'))coord.tags.push(monthTag) if (tags.includes('day'))coord.tags.push(dayTag) if (tags.includes('time'))coord.tags.push(timeTag) if (tags.includes('time')&&timeRange)coord.tags.push(timeRange) if (tags.includes('type'))coord.tags.push(typeTag) if (tags.includes('driving direction'))coord.tags.push(driDirTag) if (tags.includes('road')&&typeTag=='Official')coord.tags.push(roadTag) if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.tags.push('trekker') if (tags.includes('type')&&floorTag&&typeTag=='Official')coord.tags.push(floorTag) if (tags.includes('country'))coord.tags.push(countryTag) if (tags.includes('subdivision')&&typeTag=='Official')coord.tags.push(subdivisionTag) if (tags.includes('elevation'))coord.tags.push(elevationTag) if (tags.includes('reset heading')){ if(meta[4]) coord.heading=meta[4] } if (tags.includes('update')){ try{ const r###ltPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,50) const updatedPnaoId=r###ltPano[1][1][1] const updatedYear=r###ltPano[1][6][7][0] const updatedMonth=r###ltPano[1][6][7][1] if (coord.panoId){ if (updatedPnaoId&&updatedPnaoId!=coord.panoId) { if(panoYear!=updatedYear||panoMonth!=updatedMonth){ coord.panoId=updatedPnaoId coord.tags.push('Updated')} else{ coord.panoId=updatedPnaoId coord.tags.push('Copyright changed') } } } else{ if (panoYear&&panoMonth&&updatedYear&&updatedMonth){ if(panoYear!=updatedYear||panoMonth!=updatedMonth){ coord.panoId=updatedPnaoId coord.tags.push('Updated') } } else{ coord.panoId=svData.location.pano coord.tags.push('PanoId is added') } } } catch (error){ coord.tags.push('Failed to update') } } } catch (error) { if(!tags.includes('fix')&&!tags.includes('update'))coord.tags.push('Pano not found'); else if (tags.includes('update')){ try{ const r###ltPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,50) const updatedPnaoId=r###ltPano[1][1][1] const updatedYear=r###ltPano[1][6][7][0] const updatedMonth=r###ltPano[1][6][7][1] coord.panoId=updatedPnaoId coord.location.lat=r###ltPano[1][5][0][1][0][2] coord.location.lng=r###ltPano[1][5][0][1][0][3] } catch (error){ coord.tags.push('Failed to update') } } else{ var fixState try{ const r###ltPano=await UE('SingleImageSearch',{lat: coord.location.lat, lng: coord.location.lng},null,null,15) if(fixStrategy){ const panos=r###ltPano[1][5][0][8] for(const pano of panos){ if(pano[1][0]===panoYear&&pano[1][1]===panoMonth){ const panoIndex=pano[0] const fixedPanoId=r###ltPano[1][5][0][3][0][panoIndex][0][1] coord.panoId=fixedPanoId coord.location.lat=r###ltPano[1][5][0][1][0][2] coord.location.lng=r###ltPano[1][5][0][1][0][3] fixState=true } } } else{ coord.panoId=r###ltPano[1][1][1] coord.location.lat=r###ltPano[1][5][0][1][0][2] coord.location.lng=r###ltPano[1][5][0][1][0][3] fixState=true } } catch (error){ fixState=null } if (!fixState)coord.tags.push('Failed to fix') else coord.tags.push('Fixed') } } if (coord.tags) { coord.tags = Array.from(new Set(coord.tags))} taggedLocs.push(coord); } async function processChunk(chunk, tags) { var service = new google.maps.StreetViewService(); var panoSource= google.maps.StreetViewSource.GOOGLE var promises = chunk.map(async coord => { let panoId = coord.panoId; let latLng = {lat: coord.location.lat, lng: coord.location.lng}; let svData; let ccData; if ((panoId || latLng)) { if(tags!=['country']&&tags!=['elevation']&&tags!=['detect']){ svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50,source:panoSource}); } } if (tags.includes('generation')||tags.includes('country')||tags.includes('elevation')||tags.includes('type')||tags.includes('driving direction')||tags.includes('road')) { if(!panoId)ccData = await UE('SingleImageSearch', latLng); else ccData = await UE('GetMetadata', panoId); } if (latLng && (tags.includes('detect'))) { var detectYear,detectMonth if (coord.panoDate){ detectYear=parseInt(coord.panoDate.toISOString().substring(0,4)) detectMonth=parseInt(coord.panoDate.toISOString().substring(5,7)) } else{ if(coord.panoId){ const metaData=await getSVData(service,{pano: panoId}) if (metaData){ if(metaData.imageDate){ detectYear=parseInt(metaData.imageDate.substring(0,4)) detectMonth=parseInt(metaData.imageDate.substring(5,7)) } } } } if (detectYear&&detectMonth){ const metaData = await UE('SingleImageSearch', latLng,10); if (metaData){ if(metaData.length>1){ const defaultDate=metaData[1][6][7] if (defaultDate[0]===detectYear&&defaultDate[1]!=detectMonth){ coord.tags.push('Dangerous')} } } } } if (panoId && tags.includes('brightness')){ const brightness=await getBrightness(panoId) if (brightness<45) coord.tags.push('Dim') else if (brightness<90)coord.tags.push('Normal') else if (brightness<160)coord.tags.push('Lightful') else coord.tags.push('Overexposed') } await processCoord(coord, tags, svData,ccData) }); await Promise.all(promises); } function getSVData(service, options) { return new Promise(resolve => service.getPanorama({...options}, (data, status) => { resolve(data); })); } async function processData(tags) { let successText = 'The JSON data has been pasted to your clipboard!'; try { const totalChunks = Math.ceil(selections.length / CHUNK_SIZE); let processedChunks = 0; const swal = Swal.fire({ title: 'Tagging', text: 'If you try to tag a large number of locs by exact time, it could take quite some time. Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, icon:"info", didOpen: () => { Swal.showLoading(); } }); for (let i = 0; i < selections.length; i += CHUNK_SIZE) { let chunk = selections.slice(i, i + CHUNK_SIZE); await processChunk(chunk, tags); processedChunks++; const progress = Math.min((processedChunks / totalChunks) * 100, 100); Swal.update({ html: `<div>${progress.toFixed(2)}% completed</div> <div class="swal2-progress"> <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;"> </div> </div>` }); } swal.close(); var newJSON=[] if (exportMode) { updateLocation(selections,taggedLocs) successText = 'Tagging completed! Please save the map and refresh the page(The JSON data is also pasted to your clipboard)' } taggedLocs.forEach((loc)=>{ newJSON.push({lat:loc.location.lat, lng:loc.location.lng, heading:loc.heading, pitch: loc.pitch !== undefined && loc.pitch !== null ? loc.pitch : 90, zoom: loc.zoom !== undefined && loc.zoom !== null ? loc.zoom : 0, panoId:loc.panoId, extra:{tags:loc.tags} }) }) GM_setClipboard(JSON.stringify(newJSON)) Swal.fire({ title: 'Success!', text: successText, icon: 'success', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'OK' }) } catch (error) { swal.close(); Swal.fire('Error Tagging!', '','error'); console.error('Error processing JSON data:', error); } } } function chunkArray(array, maxSize) { const r###lt = []; for (let i = 0; i < array.length; i += maxSize) { r###lt.push(array.slice(i, i + maxSize)); } return r###lt; } function generateCheckboxHTML(tags) { const half = Math.ceil(tags.length / 2); const firstHalf = tags.slice(0, half); const secondHalf = tags.slice(half); return ` <div style="display: flex; flex-wrap: wrap; gap: 10px; text-align: left;"> <div style="flex: 1; min-width: 150px;"> ${firstHalf.map((tag, index) => ` <label style="display: block; margin-bottom: 12px; margin-left: 40px; font-size: 15px;" title="${tooltips[tag]}"> <input type="checkbox" class="feature-checkbox" value="${tag}" /> <span style="font-size: 14px;">${tag}</span> </label> `).join('')} </div> <div style="flex: 1; min-width: 150px;"> ${secondHalf.map((tag, index) => ` <label style="display: block; margin-bottom: 12px; margin-left: 40px; font-size: 15px;" title="${tooltips[tag]}"> <input type="checkbox" class="feature-checkbox" value="${tag}" /> <span style="font-size: 14px;">${tag}</span> </label> `).join('')} </div> <div style="flex: 1; min-width: 150px; margin-top: 12px; text-align: center;"> <label style="display: block; font-size: 14px;"> <input type="checkbox" class="feature-checkbox" id="selectAll" /> <span style="font-size: 16px;">Select All</span> </label> </div> </div> `; } function showFeatureSelectionPopup() { const checkboxesHTML = generateCheckboxHTML(tagBox); Swal.fire({ title: 'Select Features', html: ` ${checkboxesHTML} `, icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Start Tagging', cancelButtonText: 'Cancel', didOpen: () => { const selectAllCheckbox = Swal.getPopup().querySelector('#selectAll'); const featureCheckboxes = Swal.getPopup().querySelectorAll('.feature-checkbox:not(#selectAll)'); selectAllCheckbox.addEventListener('change', () => { featureCheckboxes.forEach(checkbox => { checkbox.checked = selectAllCheckbox.checked; }); }); featureCheckboxes.forEach(checkbox => { checkbox.addEventListener('change', () => { const allChecked = Array.from(featureCheckboxes).every(checkbox => checkbox.checked); selectAllCheckbox.checked = allChecked; }); }); }, preConfirm: () => { const selectedFeatures = []; const featureCheckboxes = Swal.getPopup().querySelectorAll('.feature-checkbox:not(#selectAll)'); featureCheckboxes.forEach(checkbox => { if (checkbox.checked) { selectedFeatures.push(checkbox.value.toLowerCase()); } }); return selectedFeatures; } }).then((r###lt) => { if (r###lt.isConfirmed) { const selectedFeatures = r###lt.value; handleSelectedFeatures(selectedFeatures); } else if (r###lt.dismiss === Swal.DismissReason.cancel) { console.log('User canceled'); } }); } function handleSelectedFeatures(features) { if (features.includes('elevation')) { Swal.fire({ title: 'Set A Range For Elevation', text: 'If you select "Cancel", the script will return the exact elevation for each location.', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel' }).then((r###lt) => { if (r###lt.isConfirmed) { Swal.fire({ title: 'Define Range for Each Segment', html: ` <label> <br>Enter range for each segment, separated by commas</br></label> <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea> `, icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, focusConfirm: false, preConfirm: () => { const segmentRangesInput = document.getElementById('segmentRanges').value.trim(); if (!segmentRangesInput) { Swal.showValidationMessage('Please enter range for each segment'); return false; } const segmentRanges = segmentRangesInput.split(','); const validatedRanges = segmentRanges.map(range => { const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/); if (matches) { const min = Number(matches[1]); const max = Number(matches[2]); return { min, max }; } else { Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue'); return false; } }); return validatedRanges.filter(Boolean); }, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', inputValidator: (value) => { if (!value.trim()) { return 'Please enter range for each segment'; } } }).then((r###lt) => { if (r###lt.isConfirmed) { runScript(features, r###lt.value); } else { Swal.showValidationMessage('You canceled input'); } }); } else if (r###lt.dismiss === Swal.DismissReason.cancel) { runScript(features); } }); } else { runScript(features); } } function calSun(date,lat,lng){ if (lat && lng && date) { const format_date = new Date(date); const times = SunCalc.getTimes(format_date, lat, lng); const sunsetTimestamp = Math.round(times.sunset.getTime() / 1000); const sunriseTimestamp = Math.round(times.sunrise.getTime() / 1000); const noonTimestamp = Math.round(times.solarNoon.getTime() / 1000); return { sunset: sunsetTimestamp, sunrise: sunriseTimestamp, noon: noonTimestamp, }; } } async function getBrightness(panoId) { 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`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.statusText}`); } const imageBlob = await response.blob(); const imageUrl = URL.createObjectURL(imageBlob); const img = new Image(); img.src = imageUrl; await new Promise((resolve) => { img.onload = resolve; }); const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = Math.floor(img.height*0.4); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; let totalBrightness = 0; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const brightness = (r + g + b) / 3; totalBrightness += brightness; } const averageBrightness = totalBrightness / (data.length / 4); URL.revokeObjectURL(imageUrl); return averageBrightness; } catch (error) { console.error('Error:', error); return null; } } var mainButton = document.createElement('button'); mainButton.textContent = 'Auto-Tag'; mainButton.id = 'main-button'; mainButton.style.position = 'fixed'; mainButton.style.right = '20px'; mainButton.style.bottom = '15px'; mainButton.style.borderRadius = '18px'; mainButton.style.padding = '5px 10px'; mainButton.style.border = 'none'; mainButton.style.color = 'white'; mainButton.style.cursor = 'pointer'; mainButton.style.backgroundColor = '#4CAF50'; mainButton.addEventListener('click', showFeatureSelectionPopup); document.body.appendChild(mainButton) })();