// ==UserScript==
// @name Geoguessr Map-Making Auto-Tag
// @namespace https://gf.qytechs.cn/users/1179204
// @version 3.86.5
// @description Tag your street view 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
// @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 60s */
let tagBox = ['Year', 'Month', 'Day', 'Time', 'Type', 'Country', 'Subdivision', 'Generation', 'Elevation', 'Driving Direction', 'Reset Heading', 'Update', 'Fix', 'Detect']
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
let tooltips = {
'Year': 'Year of street view capture in format yyyy',
'Month': 'Month of street view capture in format yy-mm',
'Day': 'Specific date of street view capture in format yyyy-mm-dd',
'Time': 'Exact time of street view capture with optional time range description, e.g., 09:35:21 marked as Morning',
'Country': 'Country of street view location (Google data)',
'Subdivision': 'Primary administrative subdivision of street view location',
'Generation': 'Camera generation of the street view, categorized as Gen1, Gen2orGen3, Gen3, Gen4, Shitcam',
'Elevation': 'Elevation of street view location (Google data)',
'Type': 'Type of street view, categorized as Official, Unofficial, Trekker (may include floor ID if available)',
'Driving Direction': 'Absolute driving direction of street view vehicle',
'Reset Heading': 'Reset heading to default driving direction of street view vehicle',
'Fix': 'Fix broken locs by updating to latest coverage or searching for specific coverage based on saved date from map-making',
'Update': 'Update street view to latest coverage or based on saved date from map-making, effective only for locs with panoID',
'Detect': 'Detect street views that are about to be removed and mark it as "Dangerous" '
};
let mapData
function getMap() {
return new Promise(function (resolve, reject) {
var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
fetch(requestURL, {
headers: {
'Accept': 'application/json',
'Content-Encoding': 'gzip'
}
})
.then(function (response) {
if (!response.ok) {
throw new Error('HTTP error, status = ' + response.status);
}
return response.json();
})
.then(function (jsonData) {
resolve(jsonData);
})
.catch(function (error) {
console.error('Fetch Error:', error);
reject('Error fetching meta data of the map!');
});
});
}
async function getSelection() {
return new Promise((resolve, reject) => {
var exportButtonText = 'Export';
var buttons = document.querySelectorAll('button.button');
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].textContent.trim() === exportButtonText) {
buttons[i].click();
var modalDialog = document.querySelector('.modal__dialog.export-modal');
}
}
setTimeout(() => {
const radioButton = document.querySelector('input[type="radio"][name="selection"][value="1"]');
const spanText = radioButton.nextElementSibling.textContent.trim();
if (spanText === "Export selection (0 locations)") {
swal.fire('Selection not found!', 'Please select at least one location as selection!', 'warning')
reject(new Error('Export selection is empty!'));
}
if (radioButton) radioButton.click()
else {
reject(new Error('Radio button not found'));
}
}, 100);
setTimeout(() => {
const copyButton = document.querySelector('.export-modal__export-buttons button:first-of-type');
if (!copyButton) {
reject(new Error('Copy button not found'));
}
copyButton.click();
}, 200);
setTimeout(() => {
const closeButton = document.querySelector('.modal__close');
if (closeButton) closeButton.click();
else reject(new Error('Close button not found'));
}, 400);
setTimeout(async () => {
try {
const data = await navigator.clipboard.readText()
const selection = JSON.parse(data);
resolve(selection);
} catch (error) {
console.error("Error getting selection:", error);
reject(error);
}
}, 800);
});
}
function matchSelection(selection, locations) {
const matchingLocations = [];
const customCoordinates = selection.customCoordinates;
const locationMap = {};
locations.forEach(loc => {
const locString = JSON.stringify(loc.location);
locationMap[locString] = loc;
});
for (const coord of customCoordinates) {
const coordString = JSON.stringify({ lat: coord.lat, lng: coord.lng });
if (locationMap.hasOwnProperty(coordString)) {
const matchingLoc = locationMap[coordString];
if (coord.extra.hasOwnProperty('panoDate') && coord.extra.panoDate) {
matchingLoc.panoDate = coord.panoDate;
}
matchingLocations.push(matchingLoc);
}
}
return matchingLocations;
}
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`;
}
function updateSelection(entries) {
var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
var payload = {
edits: []
};
entries.forEach(function (entry) {
var createEntry = {
id: -1,
author: entry.author,
mapId: entry.mapId,
location: entry.location,
panoId: entry.panoId,
panoDate: entry.panoDate,
heading: entry.heading,
pitch: entry.pitch,
zoom: entry.zoom,
tags: entry.tags,
flags: entry.flags,
createdAt: entry.createdAt,
};
payload.edits.push({
action: {
type: 3
},
create: [createEntry],
remove: [entry.id]
});
});
var xhr = new XMLHttpRequest();
xhr.open("POST", requestURL);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
console.log("Request succeeded");
} else {
console.error("Request failed with status", xhr.status);
}
};
xhr.onerror = function () {
swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Failed to update the map! Please retrieve JSON data from your clipboard.'
});
};
xhr.send(JSON.stringify(payload));
}
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 = null
}
else {
return
}
const loadingSwal = Swal.fire({
title: 'Preparing',
text: 'Fetching selected locs from map-making. Please wait...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
icon: "info",
didOpen: () => {
Swal.showLoading();
}
});
const selectedLocs = await getSelection()
mapData = await getMap()
selections = await matchSelection(selectedLocs, mapData)
loadingSwal.close()
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, s, d);
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, 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') {
var lat = coorData.lat;
var lng = coorData.lng;
lat = lat % 1 !== 0 && lat.toString().split('.')[1].length > 6 ? parseFloat(lat.toFixed(6)) : lat;
lng = lng % 1 !== 0 && lng.toString().split('.')[1].length > 6 ? parseFloat(lng.toFixed(6)) : lng;
if (s && d) {
payload = [["apiv3"], [[null, null, lat, lng], r], [[null, null, null, null, null, null, null, null, null, null, [s, d]], null, null, null, null, null, null, null, [2], null, [[[2, true, 2]]]], [[2, 6]]]
} else {
payload = [["apiv3"],
[[null, null, lat, lng], r],
[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, 10);
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) {
var year, month
array.forEach(element => {
const yearRegex1 = /^(\d{2})-(\d{2})$/;
const yearRegex2 = /^(\d{4})-(\d{2})$/;
const yearRegex3 = /^(\d{4})$/;
const monthRegex1 = /^(\d{2})$/;
const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i;
if (!year && !month && yearRegex1.test(element)) {
const match = yearRegex1.exec(element);
year = parseInt(match[1]) + 2000;
month = parseInt(match[2]);
}
if (!year && !month && yearRegex2.test(element)) {
const match = yearRegex2.exec(element);
year = parseInt(match[1]);
month = parseInt(match[2]);
}
if (!year && yearRegex3.test(element)) {
const match = 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) {
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 systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
try {
const [lat, lng] = coord;
const url = `https://api.wheretheiss.at/v1/coordinates/${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;
}
}
var CHUNK_SIZE = 1200;
if (tags.includes('time')) {
CHUNK_SIZE = 500
}
var promises = [];
async function processCoord(coord, tags, svData, ccData) {
try {
if (svData || ccData) {
var panoYear, panoMonth
if (coord.panoDate) {
panoYear = parseInt(coord.panoDate.substring(0, 4))
panoMonth = parseInt(coord.panoDate.substring(5, 7))
}
else if (coord.panoId) {
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)
}
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
let dayTag, timeTag, exactTime, timeRange
//if(monthTag){monthTag=months[monthTag-1]}
monthTag = yearTag.slice(-2) + '-' + (monthTag.toString())
if (!monthTag) { monthTag = 'Month not found' }
var date = monthToTimestamp(svData.imageDate)
if (tags.includes('day') || tags.includes('time')) {
const initialSearch = await UE('SingleImageSearch', { 'lat': coord.location.lat, 'lng': coord.location.lng }, date.startDate, date.endDate)
if (initialSearch) {
if (initialSearch.length != 3) exactTime = null;
else {
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 {
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 = 'Evening';
}
else {
timeRange = 'Night';
}
}
}
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 {
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 (trekkerTag) {
trekkerTag = trekkerTag.toString()
if (trekkerTag.includes('scout')) {
trekkerTag = 'trekker'
}
else {
trekkerTag = null
}
}
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('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 resultPano = await UE('SingleImageSearch', { lat: coord.location.lat, lng: coord.location.lng }, null, null, 10)
const updatedPnaoId = resultPano[1][1][1]
const updatedYear = resultPano[1][6][7][0]
const updatedMonth = resultPano[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.tags.push('Failed to update')
}
}
}
catch (error) {
coord.tags.push('Failed to update')
}
}
}
}
catch (error) {
if (!tags.includes('fix')) coord.tags.push('Pano not found');
else {
var fixState
try {
const resultPano = await UE('SingleImageSearch', { lat: coord.location.lat, lng: coord.location.lng }, null, null, 5)
if (fixStrategy) {
const panos = resultPano[1][5][0][8]
for (const pano of panos) {
if ((pano[1][0] === panoYear && pano[1][1] === panoMonth)) {
const panoIndex = pano[0]
const fixedPanoId = resultPano[1][5][0][3][0][panoIndex][0][1]
coord.panoId = fixedPanoId
coord.location.lat = resultPano[1][5][0][1][0][2]
coord.location.lng = resultPano[1][5][0][1][0][3]
fixState = true
}
}
}
else {
coord.panoId = resultPano[1][1][1]
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 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 });
}
}
if (tags.includes('generation') || ('country') || ('elevation') || ('type') || ('driving direction')) {
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.substring(0, 4))
detectMonth = parseInt(coord.panoDate.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 (tags != ['detect']) {
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 or elevation, 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>`
});
if (exportMode) {
updateSelection(chunk)
successText = 'Tagging completed! Do you want to refresh the page?(The JSON data is also pasted to your clipboard)'
}
}
swal.close();
var newJSON = []
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'
}).then((result) => {
if (result.isConfirmed) {
if (exportMode) {
location.reload();
}
}
});
} catch (error) {
swal.close();
Swal.fire('Error Tagging!', '', 'error');
console.error('Error processing JSON data:', error);
}
}
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'); }
}
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((result) => {
if (result.isConfirmed) {
const selectedFeatures = result.value;
handleSelectedFeatures(selectedFeatures);
} else if (result.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((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(features, result.value);
} else {
Swal.showValidationMessage('You canceled input');
}
});
} else if (result.dismiss === Swal.DismissReason.cancel) {
runScript(features);
}
});
} else {
runScript(features);
}
}
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.fontSize = '15px';
mainButton.style.padding = '10px 20px';
mainButton.style.border = 'none';
mainButton.style.color = 'white';
mainButton.style.cursor = 'pointer';
mainButton.style.backgroundColor = '#4CAF50';
mainButton.addEventListener('click', showFeatureSelectionPopup);
document.body.appendChild(mainButton)
})();