- // ==UserScript==
- // @name Pano Downloader
- // @namespace https://greasyfork.org/users/1179204
- // @version 1.2.1
- // @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.padding = "5px 10px";
- downloadButton.style.border = "none";
- downloadButton.style.color = "white";
- downloadButton.style.cursor = "pointer";
- downloadButton.style.backgroundColor = "#4CAF50";
- document.body.appendChild(downloadButton);
-
- })();