// ==UserScript==
// @name Geoguessr Map-Making Auto-Tag
// @namespace http://tampermonkey.net/
// @version 3.70
// @description Tag your street views by date&address&generation&elevation
// @author KaKa
// @match https://map-making.app/maps/*
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @license MIT
// @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 1min(60s) */
let tagBox = ['Year', 'Month','Day', 'Time','Country', 'Subdivision', 'Locality', 'Generations', 'Type', 'CoverageCount','Elevation']
async function runScript(tags,sR,uN) {
const { value: option,dismiss: inputDismiss } = await Swal.fire({
title: 'Input JSON Data',
text: 'Do you want to input data from the clipboard? If you click "Cancel", you will need to upload a JSON file.',
icon: 'question',
showCancelButton: true,
showCloseButton:true,
allowOutsideClick: false,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel'
});
let data;
if (option) {
const text = await navigator.clipboard.readText();
try {
data = JSON.parse(text);
} catch (error) {
Swal.fire('Error parsing JSON data! ', 'The input JSON data is invalid or incorrectly formatted.','error');
return;
}
} else if(inputDismiss==='cancel'){
const input = document.createElement('input');
input.type = 'file';
input.style.display = 'none'
document.body.appendChild(input);
data = await new Promise((resolve) => {
input.addEventListener('change', async () => {
const file = input.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
const result = JSON.parse(event.target.result);
resolve(result);
document.body.removeChild(input);
} catch (error) {
Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invalid or incorrectly formatted.','error');
}
};
reader.readAsText(file);
});
input.click();
});
}
const newData = [];
function mergeList(list1, list2) {
const mergedDict = list1.reduce((dict, currentValue, index) => {
if (list2[index] !== undefined) {
dict[currentValue] = list2[index];
}
return dict;
}, {});
return mergedDict;
}
async function UE(t, e,s,d) {
try {
const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
let payload=createPayload(t,e)
if(d){
payload=JSON.stringify([["apiv3"],[[null,null,e.lat,e.lng],50],[[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]]])
}
const response = await fetch(r, {
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) {
let payload;
if (mode === 'GetMetadata') {
payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData.panoId]]],[[1,2,3,4,8,6]]];
} else if (mode === 'SingleImageSearch') {
payload =[["apiv3",null,null,null,"US",null,null,null,null,null, [[0]]], [[null,null,coorData.lat,coorData.lng],50], [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,1,2],[3,1,2],[10,1,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);
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) {
if (svData) {
let levelId=svData.dn
let year = 'noyear',month = 'nomonth'
let panoType='Unofficial'
let subdivision='nosub',locality='nolocality'
let coverageCount='0'
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.time){
coverageCount = svData.time.length.toString();
}
if(svData.location.description){
let parts = svData.location.description.split(',');
if(parts.length > 1){
subdivision = parts[parts.length-1].trim();
locality = parts[parts.length-2].trim();
} else {
subdivision = svData.location.description;
}
}
return [year,month,panoType,subdivision,locality,levelId,coverageCount]
}
else{
return null}
}
function getGeneration(svData,country) {
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';
}
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'];
if (gen2Countries.includes(country)) {
return 'Gen2or3';
}
else{
return 'Gen3';}
}
else if(svData.tiles.worldSize.height === 8192){
return 'Gen4';
}
}
return 'Unknown';
}
async function getLocal(coord, timestamp) {
const apiUrl = "https://api.wheretheiss.at/v1/coordinates/";
const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
try {
const [lat, lng] = coord;
const url = `${apiUrl}${lat},${lng}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Request failed: " + response.statusText);
}
const data = await response.json();
const targetTimezoneOffset = data.offset * 3600;
const offsetDiff = systemTimezoneOffset - targetTimezoneOffset;
const convertedTimestamp = Math.round(timestamp - offsetDiff);
return convertedTimestamp;
} catch (error) {
throw error;
}
}
async function getElevation(locations) {
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`;
}
const batchSize = 100;
const totalBatches = Math.ceil(locations.length / batchSize);
for (let i = 0; i < totalBatches; i++) {
const batchLocations = locations.slice(i * batchSize, (i + 1) * batchSize);
const coordinates = batchLocations.map(location => `${location.lat},${location.lng}`).join('|');
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${coordinates}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data && data.results && data.results.length > 0) {
const elevations = data.results.map(result => result.elevation);
batchLocations.forEach((location, index) => {
if (location.extra && location.extra.tags) {
if (sR) {
const range = findRange(elevations[index], sR);
location.extra.tags.push(`${range}`);
} else {
location.extra.tags.push(`${JSON.stringify(elevations[index])}m`);
}
} else {
location.extra = {
tags: [(sR ? `${findRange(elevations[index], sR)}m` : `${JSON.stringify(elevations[index])}m`)]
};
}
});
} else {
batchLocations.forEach(location => {
if (location.extra && location.extra.tags) {
location.extra.tags.push('noElevation');
}
else{location.extra = {
tags: ['noElevation']
}
};
});
}
} catch (error) {
console.log(error);
}
}
await Promise.all(promises);
return locations;
}
var CHUNK_SIZE = 1200;
var promises = [];
async function processCoord(coord, tags, svData,ccData) {
if (!coord.extra) {
coord.extra = {};
}
if (!coord.extra.tags) {
coord.extra.tags = [];
}
if (svData){
if (svData){
let meta=getMetaData(svData)
let yearTag=meta[0]
let monthTag=meta[1]
let typeTag=meta[2]
let subdivisionTag=meta[3]
let localityTag=meta[4]
let countryTag
let genTag
let trekkerTag=meta[5]
let coverageTag=meta[6]
let dayTag,timeTag,exactTime,timeRange
var date=monthToTimestamp(meta[0]+'-'+meta[1])
if(tags.includes('day')||tags.includes('time')){
exactTime=await binarySearch(coord, date.startDate,date.endDate)
if (exactTime<=date.startDate||exactTime>=date.endDate){
exactTime=null
}
}
if(!exactTime){dayTag='noday'
timeTag='notime'
}
else{
const dayOffset = Math.round((coord.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.lat,coord.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 = 'Evening';
}
else{
timeRange = 'Night';
}
}
}
if (ccData){
try {
countryTag = ccData[1][0][5][0][1][4]
}
catch (error) {
try {
countryTag = ccData[1][5][0][1][4]
} catch (error) {
countryTag='nocountry'
}
}
if (!countryTag)countryTag='nocountry'
}
if (tags.includes('generation')&&typeTag=='Official'){
genTag = getGeneration(svData,countryTag)
coord.extra.tags.push(genTag)}
if (tags.includes('year'))coord.extra.tags.push(yearTag)
if (tags.includes('month'))coord.extra.tags.push(yearTag.slice(-2)+'-'+monthTag)
if (tags.includes('day'))coord.extra.tags.push(dayTag)
if (tags.includes('time')) coord.extra.tags.push(timeTag)
if (tags.includes('time')&&timeRange) coord.extra.tags.push(timeRange)
if (tags.includes('type'))coord.extra.tags.push(typeTag)
if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.extra.tags.push('trekker')
if (tags.includes('country')&&typeTag=='Official')coord.extra.tags.push(countryTag)
if (tags.includes('subdivision')&&typeTag=='Official')coord.extra.tags.push(subdivisionTag)
if (tags.includes('locality')&&typeTag=='Official')coord.extra.tags.push(localityTag)
if (tags.includes('coverageCount')&&typeTag=='Official')coord.extra.tags.push(coverageTag)
}
}
else {
if(tags.some(tag => tagBox.includes(tag))){
coord.extra.tags.push('nopano')
}
}
if (coord.extra.tags) {coord.extra.tags=Array.from(new Set(coord.extra.tags))}
newData.push(coord);
}
async function processChunk(chunk, tags) {
if (tags.includes('elevation')){
try {
chunk = await getElevation(chunk);
} catch (error) {
console.error('error fecthing elevtion data:', error);
}
}
var service = new google.maps.StreetViewService();
var promises = chunk.map(async coord => {
let panoId = coord.panoId;
if (!panoId) {
if (coord.extra&&coord.extra.panoId){
panoId = coord.extra.panoId;}
}
let latLng = {lat: coord.lat, lng: coord.lng};
let svData;
let ccData;
if ((panoId || latLng)) {
if(tags!=['elevation']){
svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50});}
}
if (!panoId && (tags.includes('generation')||('country'))) {
ccData = await UE('SingleImageSearch', coord);
} else if (panoId && (tags.includes('generation')||('country'))) {
ccData = await UE('GetMetadata', coord);
}
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) {
try {
const totalChunks = Math.ceil(data.customCoordinates.length / CHUNK_SIZE);
let processedChunks = 0;
const swal = Swal.fire({
title: 'Processing Data',
text: 'Please wait...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
didOpen: () => {
Swal.showLoading();
}
});
for (let i = 0; i < data.customCoordinates.length; i += CHUNK_SIZE) {
let chunk = data.customCoordinates.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>`
});
}
GM_setClipboard(JSON.stringify(newData));
swal.close();
Swal.fire( 'Success!','New JSON data has been copied to the clipboard!','success');
} catch (error) {
swal.close();
Swal.fire('Error!', 'Invalid JSON data','error');
console.error('Error processing JSON data:', error);
}
}
if(data.customCoordinates){
if(data.customCoordinates.length>=1){processData(tags);}
else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is empty.','error');}
}else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');}
}
function createCheckbox(text, tags) {
var label = document.createElement('label');
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = text;
checkbox.name = 'tags';
checkbox.id = tags;
label.appendChild(checkbox);
label.appendChild(document.createTextNode(text));
buttonContainer.appendChild(label);
return checkbox;
}
var mainButton = document.createElement('button');
mainButton.textContent = 'Auto-Tag';
mainButton.style.position = 'fixed';
mainButton.style.right = '20px';
mainButton.style.bottom = '20px';
mainButton.style.borderRadius = "18px";
mainButton.style.fontSize ="16px";
mainButton.style.padding = "10px 20px";
mainButton.style.border = "none";
mainButton.style.color = "white";
mainButton.style.cursor = "pointer";
mainButton.style.backgroundColor = "#4CAF50";
mainButton.addEventListener('click', function() {
if (buttonContainer.style.display === 'none') {
buttonContainer.style.display = 'block';
} else {
buttonContainer.style.display = 'none';
}
});
document.body.appendChild(mainButton);
var buttonContainer = document.createElement('div');
buttonContainer.style.position = 'fixed';
buttonContainer.style.right = '20px';
buttonContainer.style.bottom = '60px';
buttonContainer.style.display = 'none';
buttonContainer.style.fontSize='15px'
document.body.appendChild(buttonContainer);
var triggerButton = document.createElement('button');
triggerButton.textContent = 'Star Tagging';
triggerButton.addEventListener('click', function() {
var checkboxes = document.getElementsByName('tags');
var checkedTags = [];
for (var i=0; i<checkboxes.length; i++) {
if (checkboxes[i].checked) {
checkedTags.push(checkboxes[i].id);
}
}
if (checkedTags.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((result) => {
if (result.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((result) => {
if (result.isConfirmed) {
runScript(checkedTags,result.value)
} else {
Swal.showValidationMessage('You canceled input');
}
});}
else if (result.dismiss === Swal.DismissReason.cancel){runScript(checkedTags)}
});
}
else{
runScript(checkedTags)}
})
buttonContainer.appendChild(triggerButton);
tagBox.forEach(tag => {
createCheckbox(tag, tag.toLowerCase());
});
})();