您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
download panoramas from google
当前为
// ==UserScript== // @name Pano Downloader // @namespace https://greasyfork.org/users/1179204 // @version 1.4.5 // @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 zoomLevel function getSelection() { const activeSelections = unsafeWindow.editor.selections return activeSelections.flatMap(selection => selection.locations) } function stripBaidu(pano) { return pano.replace("BAIDU:", ""); } function stripTencent(pano) { return pano.replace("TENCENT:", ""); } 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=getSelection() if(selectedLocs.length>0) { const { value: options, dismiss: inputDismiss } = await Swal.fire({ title: 'Download Options', html: '<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; margin:5px; 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>'+ '<select id="img-select" class="swal2-input" style="width:180px; height:40px; margin:5px; font-size:16px;white-space:prewrap">' + '<option value="1">Equirectangular</option>' + '<option value="2">Perspective</option>' + '<option value="3">Thumbnail</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,document.getElementById('img-select').value]; } }); if (options){ zoomLevel=parseInt(options[0]) processData(selectedLocs,parseInt(options[1])) }} } 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(); }); } function generatePerspective(canvas, FOV, THETA, PHI, outputWidth, outputHeight) { const perspectiveCanvas = document.createElement('canvas'); perspectiveCanvas.width = outputWidth; perspectiveCanvas.height = outputHeight; const perspectiveCtx = perspectiveCanvas.getContext('2d'); const f = 0.5 * outputWidth / Math.tan((FOV / 2) * (Math.PI / 180)); const cx = outputWidth / 2; const cy = outputHeight / 2; var inputWidth = canvas.width; var inputHeight = canvas.height; const inputCtx = canvas.getContext('2d'); const inputImageData = inputCtx.getImageData(0, 0, inputWidth, inputHeight); const outputImageData = perspectiveCtx.createImageData(outputWidth, outputHeight); const outputData = outputImageData.data; const R1 = rotationMatrix([0, 1, 0], THETA); const rotatedXAxis = applyRotation(R1, [1, 0, 0]); const R2 = rotationMatrix(rotatedXAxis, PHI); const R = multiplyMatrices(R2, R1); for (let y = 0; y < outputHeight; y++) { for (let x = 0; x < outputWidth; x++) { const nx = (x - cx) / f; const ny = (y - cy) / f; const nz = 1; const [rx, ry, rz] = applyRotation(R, [nx, ny, nz]); const lon = Math.atan2(rx, rz); const lat = Math.asin(ry / Math.sqrt(rx * rx + ry * ry + rz * rz)); const u = Math.floor(((lon / (2 * Math.PI)) + 0.5) * inputWidth); const v = Math.floor(((lat / Math.PI) + 0.5) * inputHeight); if (u >= 0 && u < inputWidth && v >= 0 && v < inputHeight) { const srcOffset = (v * inputWidth + u) * 4; const destOffset = (y * outputWidth + x) * 4; outputData[destOffset] = inputImageData.data[srcOffset]; // Red outputData[destOffset + 1] = inputImageData.data[srcOffset + 1]; // Green outputData[destOffset + 2] = inputImageData.data[srcOffset + 2]; // Blue outputData[destOffset + 3] = 255; // Alpha } } } perspectiveCtx.putImageData(outputImageData, 0, 0); return perspectiveCanvas; } function rotationMatrix(axis, angle) { const rad = angle * (Math.PI / 180); const c = Math.cos(rad); const s = Math.sin(rad); const t = 1 - c; const [x, y, z] = axis; return [ [t*x*x + c, t*x*y - s*z, t*x*z + s*y], [t*x*y + s*z, t*y*y + c, t*y*z - s*x], [t*x*z - s*y, t*y*z + s*x, t*z*z + c] ]; } function applyRotation(matrix, vector) { return [ matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2], matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2], matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2] ]; } function multiplyMatrices(A, B) { const result = Array(3).fill(null).map(() => Array(3).fill(0)); for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { for (let k = 0; k < 3; k++) { result[i][j] += A[i][k] * B[k][j]; } } } return result; } async function downloadPanoramaImage(panoId, fileName,w,h,th,ch,tp,mode) { return new Promise(async (resolve, reject) => { try { let canvas, ctx, tilesPerRow, tilesPerColumn, tileUrl, imageUrl; const tileWidth = 512; const tileHeight = 512; if (panoId.includes('BAIDU')||panoId.includes('TENCENT')) { tilesPerRow = 16; tilesPerColumn = 8; } else { let zoomTiles; imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=0&fover=2`; zoomTiles = [2, 4, 8, 16, 32]; tilesPerRow = Math.min(Math.ceil(w / tileWidth), zoomTiles[zoomLevel - 1]); tilesPerColumn = Math.min(Math.ceil(h / tileHeight), zoomTiles[zoomLevel - 1] / 2); } canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); canvas.width = tilesPerRow * tileWidth; canvas.height = tilesPerColumn * tileHeight; if (w === 13312) { const sizeMap = { 4: [6656, 3328], 3: [3328, 1664], 2: [1664, 832], 1: [832, 416] }; if (sizeMap[zoomLevel]) { [canvas.width, canvas.height] = sizeMap[zoomLevel]; } } const loadTile = (x, y) => { return new Promise(async (resolveTile) => { let tile; if (panoId.includes('TENCENT')) { tileUrl = `https://sv4.map.qq.com/tile?svid=${stripTencent(panoId)}&x=${x}&y=${y}&from=web&level=1`; } else if (panoId.includes('BAIDU')) { tileUrl = `https://mapsv0.bdimg.com/?qt=pdata&sid=${stripBaidu(panoId)}&pos=${y}_${x}&z=5`; } else { tileUrl = `${imageUrl}&x=${x}&y=${y}`; } try { tile = await loadImage(tileUrl); ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight); resolveTile(); } catch (error) { console.error(`Error loading tile at ${x},${y}:`, error); resolveTile(); } }); }; let tilePromises = []; for (let y = 0; y < tilesPerColumn; y++) { for (let x = 0; x < tilesPerRow; x++) { tilePromises.push(loadTile(x, y)); } } await Promise.all(tilePromises); if(mode!=1){ var targetTheta if(th||th==0) targetTheta=(th-ch) else targetTheta=0 const perspectiveCanvas = generatePerspective(canvas, 125,targetTheta,tp,1920, 1080) perspectiveCanvas.toBlob(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName+'.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); resolve(); }, 'image/png');} else{ canvas.toBlob(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName+'.jpg'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); resolve(); }, 'image/jpg'); } } catch (error) { Swal.fire('Error!', error.toString(),'error'); reject(error); } }); } async function downloadPanoThumbnail(panoId, fileName, h,p) { const url = `https://streetviewpixels-pa.googleapis.com/v1/thumbnail?panoid=${panoId}&cb_client=maps_sv.tactile.gps&yaw=${h}&pitch=${p}&thumbfov=120&width=1024&height=512`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error: ${response.status}`); const blob = await response.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `${fileName}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); console.log(`Failed to download: ${fileName}.png`); } catch (error) { console.error(`Failed to download (${fileName}):`, 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,mode) { var service = new google.maps.StreetViewService(); var promises = chunk.map(async coord => { let panoId = coord.panoId; const th=coord.heading const tp=coord.pitch let latLng = {lat: coord.location.lat, lng: coord.location.lng}; let svData; if ((panoId || latLng)) { svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 5}); } if (svData.tiles&&svData.tiles.worldSize) { const w=svData.tiles.worldSize.width const h=svData.tiles.worldSize.height const ch=svData.tiles.centerHeading const fileName = panoId; if(mode==3) await downloadPanoThumbnail(panoId,fileName,th,tp) else await downloadPanoramaImage(panoId, fileName,w,h,th,ch,tp,mode); } }); await Promise.all(promises); } function getSVData(service, options) { return new Promise(resolve => service.getPanorama({...options}, (data, status) => { resolve(data); })); } async function processData(panos,mode) { try { let processedChunks = 0; 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,mode); 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); })();