Pano Downloader

download panoramas from google

目前为 2024-11-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Pano Downloader
  3. // @namespace https://greasyfork.org/users/1179204
  4. // @version 1.2.1
  5. // @description download panoramas from google
  6. // @author KaKa
  7. // @match https://map-making.app/maps/*
  8. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  9. // @license MIT
  10. // @icon https://www.svgrepo.com/show/502638/download-photo.svg
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15. let mapData,zoomLevel
  16.  
  17. function getMap() {
  18. return new Promise(function(resolve, reject) {
  19. var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
  20.  
  21. fetch(requestURL)
  22. .then(function(response) {
  23. if (!response.ok) {
  24. throw new Error('HTTP error, status = ' + response.status);
  25. }
  26. return response.json();
  27. })
  28. .then(function(jsonData) {
  29. resolve(jsonData);
  30. })
  31. .catch(function(error) {
  32. console.error('Fetch Error:', error);
  33. reject('Error fetching meta data of the map!');
  34. });
  35. });
  36. }
  37.  
  38. async function getSelection() {
  39. return new Promise((resolve, reject) => {
  40. var exportButtonText = 'Export';
  41. var buttons = document.querySelectorAll('button.button');
  42.  
  43. for (var i = 0; i < buttons.length; i++) {
  44. if (buttons[i].textContent.trim() === exportButtonText) {
  45. buttons[i].click();
  46. var modalDialog = document.querySelector('.modal__dialog.export-modal');
  47. }
  48. }
  49.  
  50. setTimeout(() => {
  51. const radioButton = document.querySelector('input[type="radio"][name="selection"][value="1"]');
  52. const spanText = radioButton.nextElementSibling.textContent.trim();
  53. if (spanText==="Export selection (0 locations)") {
  54. swal.fire('Selection not found!', 'Please select at least one panorama as selection!','warning')
  55. reject(new Error('Export selection is empty!'));
  56. }
  57. if (radioButton) radioButton.click()
  58. else{
  59. reject(new Error('Radio button not found'));}
  60. }, 100);
  61.  
  62.  
  63. setTimeout(() => {
  64. const copyButton = document.querySelector('.export-modal__export-buttons button:first-of-type');
  65. if (!copyButton) {
  66. reject(new Error('Copy button not found'));
  67. }
  68. copyButton.click();
  69.  
  70. }, 200);
  71. setTimeout(() => {
  72. const closeButton = document.querySelector('.modal__close');
  73. if (closeButton) closeButton.click();
  74. else reject(new Error('Close button not found'));
  75. }, 400);
  76.  
  77. setTimeout(async () => {
  78. try {
  79. const data = await navigator.clipboard.readText()
  80. const selection = JSON.parse(data);
  81. resolve(selection);
  82. } catch (error) {
  83. console.error("Error getting selection:", error);
  84. reject(error);
  85. }
  86. }, 800);
  87. });
  88. }
  89.  
  90. function matchSelection(selection, locations) {
  91. const matchingLocations = [];
  92. const customCoordinates = selection.customCoordinates;
  93.  
  94. const locationSet = new Set(locations.map(loc => JSON.stringify(loc.location)));
  95.  
  96. for (const coord of customCoordinates) {
  97. const coordString = JSON.stringify({ lat: coord.lat, lng: coord.lng });
  98.  
  99. if (locationSet.has(coordString)) {
  100. const matchingLoc = locations.find(loc => JSON.stringify(loc.location) === coordString);
  101. if (matchingLoc) {
  102. matchingLocations.push(matchingLoc);
  103. }
  104. }
  105. }
  106. return matchingLocations;
  107. }
  108.  
  109. async function runScript() {
  110.  
  111. const { value: option,dismiss: inputDismiss } = await Swal.fire({
  112. title: 'Select Panoramas',
  113. 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.',
  114. icon: 'question',
  115. showCancelButton: true,
  116. showCloseButton:true,
  117. allowOutsideClick: false,
  118. confirmButtonColor: '#3085d6',
  119. cancelButtonColor: '#d33',
  120. confirmButtonText: 'Yes',
  121. cancelButtonText: 'Cancel'
  122. });
  123.  
  124.  
  125. if (option) {
  126.  
  127. const selectedLocs=await getSelection()
  128. mapData=await getMap()
  129. const data=await matchSelection(selectedLocs,mapData)
  130. if(data) {
  131. const { value: zoom, dismiss: inputDismiss } = await Swal.fire({
  132. title: 'Zoom Level',
  133. html:
  134. '<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' +
  135. '<option value="1">1 (100KB~500KB)</option>' +
  136. '<option value="2">2 (500KB~1MB)</option>' +
  137. '<option value="3">3 (1MB~4MB)</option>' +
  138. '<option value="4">4 (4MB~8MB)</option>' +
  139. '<option value="5">5 (8MB~24MB)</option>' +
  140. '</select>',
  141. icon: 'question',
  142. showCancelButton: true,
  143. showCloseButton: true,
  144. allowOutsideClick: false,
  145. confirmButtonColor: '#3085d6',
  146. cancelButtonColor: '#d33',
  147. confirmButtonText: 'Yes',
  148. cancelButtonText: 'Cancel',
  149. preConfirm: () => {
  150. return document.getElementById('zoom-select').value;
  151. }
  152. });
  153. if (zoom){
  154. zoomLevel=parseInt(zoom)
  155. processData(data)
  156. }}
  157. }
  158.  
  159. else if(inputDismiss==='cancel'){
  160.  
  161. const input = document.createElement('input');
  162. input.type = 'file';
  163. input.style.display = 'none'
  164. document.body.appendChild(input);
  165.  
  166. const data = await new Promise((resolve) => {
  167. input.addEventListener('change', async () => {
  168. const file = input.files[0];
  169. const reader = new FileReader();
  170.  
  171. reader.onload = (event) => {
  172. try {
  173. const result = JSON.parse(event.target.result);
  174. resolve(result);
  175.  
  176. document.body.removeChild(input);
  177. } catch (error) {
  178. Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invalid or incorrectly formatted.','error');
  179. }
  180. };
  181.  
  182. reader.readAsText(file);
  183. });
  184.  
  185.  
  186. input.click();
  187. });
  188. }
  189.  
  190. async function downloadPanoramaImage(panoId, fileName,panoramaWidth,panoramaHeight) {
  191. return new Promise(async (resolve, reject) => {
  192. try {
  193. const imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=1&fover=2`;
  194. const tileWidth = 512;
  195. const tileHeight = 512;
  196. const zoomTiles=[2,4,8,16,32]
  197.  
  198. const tilesPerRow = Math.min(Math.ceil(panoramaWidth / tileWidth),zoomTiles[zoomLevel-1]);
  199. const tilesPerColumn = Math.min(Math.ceil(panoramaHeight / tileHeight),zoomTiles[zoomLevel-1]/2);
  200.  
  201. const canvasWidth = tilesPerRow * tileWidth;
  202. const canvasHeight = tilesPerColumn * tileHeight;
  203.  
  204. const canvas = document.createElement('canvas');
  205. const ctx = canvas.getContext('2d');
  206. canvas.width = canvasWidth;
  207. canvas.height = canvasHeight;
  208.  
  209. for (let y = 0; y < tilesPerColumn; y++) {
  210. for (let x = 0; x < tilesPerRow; x++) {
  211. const tileUrl = `${imageUrl}&x=${x}&y=${y}`;
  212. const tile = await loadImage(tileUrl);
  213. ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
  214. }
  215. }
  216.  
  217. canvas.toBlob(blob => {
  218. const url = window.URL.createObjectURL(blob);
  219. const a = document.createElement('a');
  220. a.href = url;
  221. a.download = fileName;
  222. document.body.appendChild(a);
  223. a.click();
  224. window.URL.revokeObjectURL(url);
  225. resolve();
  226. }, 'image/jpeg');
  227. } catch (error) {
  228. Swal.fire('Error!', error.toString(),'error');
  229. reject(error);
  230. }
  231. });
  232. }
  233.  
  234. async function loadImage(url) {
  235. return new Promise((resolve, reject) => {
  236. const img = new Image();
  237. img.crossOrigin = 'Anonymous';
  238. img.onload = () => resolve(img);
  239. img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
  240. img.src = url;
  241. });
  242. }
  243.  
  244. var CHUNK_SIZE = Math.round(20/zoomLevel);
  245. var promises = [];
  246.  
  247. async function processChunk(chunk) {
  248. var service = new google.maps.StreetViewService();
  249. var promises = chunk.map(async coord => {
  250. let panoId = coord.panoId;
  251. if (!panoId && coord.extra.panoId) {
  252. panoId = coord.extra.panoId;
  253. }
  254. let latLng = {lat: coord.lat, lng: coord.lng};
  255. let svData;
  256.  
  257. if ((panoId || latLng)) {
  258. svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 5});
  259. }
  260.  
  261.  
  262. if (svData.tiles.worldSize) {
  263. const w=svData.tiles.worldSize.width
  264. const h=svData.tiles.worldSize.height
  265. const fileName = `${panoId}.jpg`;
  266. await downloadPanoramaImage(panoId, fileName,w,h);
  267. }
  268.  
  269. });
  270.  
  271. await Promise.all(promises);
  272. }
  273.  
  274. function getSVData(service, options) {
  275. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  276. resolve(data);
  277. }));
  278. }
  279.  
  280. async function processData(data) {
  281. let panos
  282. try {
  283. let processedChunks = 0;
  284. panos=data.customCoordinates
  285. if (!panos)panos=data
  286.  
  287. const swal = Swal.fire({
  288. title: 'Downloading',
  289. text: 'Please wait...',
  290. allowOutsideClick: false,
  291. allowEscapeKey: false,
  292. showConfirmButton: false,
  293. didOpen: () => {
  294. Swal.showLoading();
  295. }
  296. });
  297. for (let i = 0; i < panos.length; i += 5) {
  298. let chunk = panos.slice(i, i + 5);
  299. await processChunk(chunk);
  300. processedChunks++;
  301. const progress = Math.min((processedChunks /panos.length) * 100, 100);
  302. Swal.update({
  303. html: `<div>${progress.toFixed(2)}% completed</div>
  304. <div class="swal2-progress">
  305. <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;">
  306. </div>
  307. </div>`
  308. });
  309. }
  310. swal.close();
  311. Swal.fire('Success!','Download completed', 'success');
  312. } catch (error) {
  313. swal.close();
  314. Swal.fire('Error!',"Failed to download due to:"+error.toString(),'error');
  315. }
  316. }
  317. }
  318.  
  319. var downloadButton=document.createElement('button');
  320. downloadButton.textContent='Download Panos'
  321. downloadButton.addEventListener('click', runScript);
  322. downloadButton.style.width='160px'
  323. downloadButton.style.position = 'fixed';
  324. downloadButton.style.right = '150px';
  325. downloadButton.style.bottom = '15px';
  326. downloadButton.style.borderRadius = "18px";
  327. downloadButton.style.padding = "5px 10px";
  328. downloadButton.style.border = "none";
  329. downloadButton.style.color = "white";
  330. downloadButton.style.cursor = "pointer";
  331. downloadButton.style.backgroundColor = "#4CAF50";
  332. document.body.appendChild(downloadButton);
  333.  
  334. })();