// ==UserScript==
// @name Pano Downloader
// @namespace https://greasyfork.org/users/1179204
// @version 1.2
// @description download panoramas from google
// @author KaKa
// @match https://map-making.app/maps/*
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @license MIT
// @icon https://www.svgrepo.com/show/502638/download-photo.svg
// ==/UserScript==
(function() {
'use strict';
let mapData,zoomLevel
function getMap() {
return new Promise(function(resolve, reject) {
var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
fetch(requestURL)
.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 panorama 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 locationSet = new Set(locations.map(loc => JSON.stringify(loc.location)));
for (const coord of customCoordinates) {
const coordString = JSON.stringify({ lat: coord.lat, lng: coord.lng });
if (locationSet.has(coordString)) {
const matchingLoc = locations.find(loc => JSON.stringify(loc.location) === coordString);
if (matchingLoc) {
matchingLocations.push(matchingLoc);
}
}
}
return matchingLocations;
}
async function runScript() {
const { value: option,dismiss: inputDismiss } = await Swal.fire({
title: 'Select Panoramas',
text: 'Do you want to input the panoId from your selections on map-making? 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'
});
if (option) {
const selectedLocs=await getSelection()
mapData=await getMap()
const data=await matchSelection(selectedLocs,mapData)
if(data) {
const { value: zoom, dismiss: inputDismiss } = await Swal.fire({
title: 'Zoom Level',
html:
'<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' +
'<option value="1">1 (100KB~500KB)</option>' +
'<option value="2">2 (500KB~1MB)</option>' +
'<option value="3">3 (1MB~4MB)</option>' +
'<option value="4">4 (4MB~8MB)</option>' +
'<option value="5">5 (8MB~24MB)</option>' +
'</select>',
icon: 'question',
showCancelButton: true,
showCloseButton: true,
allowOutsideClick: false,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
preConfirm: () => {
return document.getElementById('zoom-select').value;
}
});
if (zoom){
zoomLevel=parseInt(zoom)
processData(data)
}}
}
else if(inputDismiss==='cancel'){
const input = document.createElement('input');
input.type = 'file';
input.style.display = 'none'
document.body.appendChild(input);
const 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();
});
}
async function downloadPanoramaImage(panoId, fileName,panoramaWidth,panoramaHeight) {
return new Promise(async (resolve, reject) => {
try {
const imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=1&fover=2`;
const tileWidth = 512;
const tileHeight = 512;
const zoomTiles=[2,4,8,16,32]
const tilesPerRow = Math.min(Math.ceil(panoramaWidth / tileWidth),zoomTiles[zoomLevel-1]);
const tilesPerColumn = Math.min(Math.ceil(panoramaHeight / tileHeight),zoomTiles[zoomLevel-1]/2);
const canvasWidth = tilesPerRow * tileWidth;
const canvasHeight = tilesPerColumn * tileHeight;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
for (let y = 0; y < tilesPerColumn; y++) {
for (let x = 0; x < tilesPerRow; x++) {
const tileUrl = `${imageUrl}&x=${x}&y=${y}`;
const tile = await loadImage(tileUrl);
ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
}
}
canvas.toBlob(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
resolve();
}, 'image/jpeg');
} catch (error) {
Swal.fire('Error!', error.toString(),'error');
reject(error);
}
});
}
async function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
img.src = url;
});
}
var CHUNK_SIZE = Math.round(20/zoomLevel);
var promises = [];
async function processChunk(chunk) {
var service = new google.maps.StreetViewService();
var promises = chunk.map(async coord => {
let panoId = coord.panoId;
if (!panoId && coord.extra.panoId) {
panoId = coord.extra.panoId;
}
let latLng = {lat: coord.lat, lng: coord.lng};
let svData;
if ((panoId || latLng)) {
svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 5});
}
if (svData.tiles.worldSize) {
const w=svData.tiles.worldSize.width
const h=svData.tiles.worldSize.height
const fileName = `${panoId}.jpg`;
await downloadPanoramaImage(panoId, fileName,w,h);
}
});
await Promise.all(promises);
}
function getSVData(service, options) {
return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
resolve(data);
}));
}
async function processData(data) {
let panos
try {
let processedChunks = 0;
panos=data.customCoordinates
if (!panos)panos=data
const swal = Swal.fire({
title: 'Downloading',
text: 'Please wait...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
didOpen: () => {
Swal.showLoading();
}
});
for (let i = 0; i < panos.length; i += 5) {
let chunk = panos.slice(i, i + 5);
await processChunk(chunk);
processedChunks++;
const progress = Math.min((processedChunks /panos.length) * 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();
Swal.fire('Success!','Download completed', 'success');
} catch (error) {
swal.close();
Swal.fire('Error!',"Failed to download due to:"+error.toString(),'error');
}
}
}
var downloadButton=document.createElement('button');
downloadButton.textContent='Download Panos'
downloadButton.addEventListener('click', runScript);
downloadButton.style.width='160px'
downloadButton.style.position = 'fixed';
downloadButton.style.right = '150px';
downloadButton.style.bottom = '15px';
downloadButton.style.borderRadius = "18px";
downloadButton.style.fontSize = "15px";
downloadButton.style.padding = "10px 20px";
downloadButton.style.border = "none";
downloadButton.style.color = "white";
downloadButton.style.cursor = "pointer";
downloadButton.style.backgroundColor = "#4CAF50";
document.body.appendChild(downloadButton);
})();