- // ==UserScript==
- // @name Geoguessr Map-Making Auto-Tag
- // @namespace http://tampermonkey.net/
- // @version 3.61
- // @description Tag your street views by date&address&generations
- // @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.google.com/s2/favicons?domain=geoguessr.com
- // ==/UserScript==
-
- (function() {
- 'use strict';
- let accuracy=86400
- let tagBox = ['Year', 'Month','Day', 'Country', 'Subdivision', 'Locality', 'Generations', 'Type', 'CoverageCount','Elevation']
-
- async function runScript(tags,sR) {
- 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 = [];
-
- 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],3],[[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
- while (end - start >= accuracy) {
- let mid= Math.round((start + end) / 2);
- let 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 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){
- 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
- var date=monthToTimestamp(meta[0]+'-'+meta[1])
- let exactTime=await binarySearch(coord, date.startDate,date.endDate)
-
- if(!exactTime){dayTag='noday'}
- else{var dateObject = new Date(exactTime * 1000);
- var formattedDate = dateObject.toISOString().split('T')[0];
- dayTag=formattedDate}
-
- 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'
- }
-
- genTag = getGeneration(svData,countryTag)
- if (tags.includes('generation')&&typeTag=='Official')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('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)) {
- 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({
- title: 'Success!',
- text: 'New JSON data has been copied to the clipboard!',
- icon: 'success'
- });
- } catch (error) {
- swal.close();
- Swal.fire({
- title: 'Error!',
- text: 'Invalid JSON data',
- icon: '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';
- 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: 'Please set a range for the elevation. 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{runScript(checkedTags)}
- });
- }
- else{
- runScript(checkedTags)}
- })
- buttonContainer.appendChild(triggerButton);
-
- tagBox.forEach(tag => {
- createCheckbox(tag, tag.toLowerCase());
- });
- })();