您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
download panoramas from google
当前为
// ==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); })();