From 49622229010a9b6c9b45720577b97f5d3eb70163 Mon Sep 17 00:00:00 2001 From: Alexander Bass Date: Fri, 23 Sep 2022 22:11:44 -0400 Subject: [PATCH] Initial Commit --- index.html | 52 +++++++++++ index.js | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 127 +++++++++++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 index.html create mode 100644 index.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..d87521b --- /dev/null +++ b/index.html @@ -0,0 +1,52 @@ + + + + + GSV image viewer + + + + +
+
+

GSV image viewer

+ +
+ +
+
+

+ This tool allows you to view and download the full panoramic imagery from google's street view and google's photo spheres. To view an image, copy the link of the google street view page, paste it into the `input URL` field, and press `Load Image`. The resolution of the image downloaded can be adjusted with the `Resolution Level` field. +

+ + + +
+ +
+ Status: N/A | + Image Type: N/A | + Image ID: N/A | + Zoom Levels: N/A | + Copyright: N/A +
+ +
+ +

Controls

+

Input URL

+ + +

Resolution Level

+ + + + Note, this tool is subject to your browsers limitations of HTML canvas. Higher resolution images are rendered to two canvases to bypass the single canvas area limit. The download button may not work with very large images. If the download button does not work, right click and `Save Image As...` on both the top and bottom canvases. + +
+ + This tool is not in any way affiliated or endorsed with or by google, google maps, or google street view.
Use at your own risk and respect for copyright +
+
+ + diff --git a/index.js b/index.js new file mode 100644 index 0000000..caeb19a --- /dev/null +++ b/index.js @@ -0,0 +1,253 @@ +'use strict'; + +// +// VARIABLES +// + +var canvas, canvas2, ctx, ctx2, zoom; +const maxCanvasHeight = 7000; //px +var downloadCount = 0; + +// +// UTILITY FUNCTIONS +// + +function ID(id) { + return document.getElementById(id); +} + +function extractTextBetween(input, before, after) { + const step1 = input.substring(input.indexOf(before) + before.length, input.length); + const step2 = step1.substring(0, step1.indexOf(after)); + return step2; +} + +// +// INITIALIZATION +// + +window.addEventListener("load", () => { + const urlBox = ID("downloadURL"); + canvas = ID("canvas"); + canvas2 = ID("canvas2"); + ctx = canvas.getContext("2d"); + ctx2 = canvas2.getContext("2d"); + + ID("loadButton").addEventListener("click", () => { + zoom = ID("zoom").value; + console.log("URL : ", urlBox.value); + parseURL(urlBox.value); + }); + ID("downloadButton").addEventListener("click", () => { + downloadCanvas(); + }); +}); + +// +// +// + +function downloadCanvas() { + ID("infoStatus").textContent = "saving image.."; + if (canvas2.height > 0) { + const link1 = document.createElement('a'); + link1.download = `top${downloadCount}.png`; + link1.href = canvas.toDataURL(); + link1.click(); + const link2 = document.createElement('a'); + link2.download = `bottom${downloadCount}.png`; + link2.href = canvas2.toDataURL(); + link2.click(); + downloadCount++; + } else { + const link = document.createElement('a'); + link.download = `image${downloadCount}.png`; + link.href = canvas.toDataURL(); + link.click(); + downloadCount++; + } +} + +function resetInfoDisplay() { + const normal = "N/A"; + ID("infoStatus").textContent = normal; + ID("infoType").textContent = normal; + ID("infoCopyright").textContent = normal; + ID("infoZooms").textContent = normal; + ID("infoID").textContent = normal; +} + +function parseURL(url) { + resetInfoDisplay(); + if (url.includes("panoid")) { + const id = extractTextBetween(url, "panoid%3D", "%"); + console.log("Extracted PanoID using method 1: ", id); + + getPath(id); + } else if (url.includes("m4!1s") && url.includes("data=")) { + const id = extractTextBetween(url, "m4!1s", "!2e"); + if (id.length > 22) { + console.log("Extracted Photosphere ID using method 2: ", id); + getSphere(id); + } else { + console.log("Extracted PanoID using method 2: ", id); + getPath(id); + } + } else if (url.includes("googleusercontent.com")) { + const id = extractTextBetween(url, "googleusercontent.com%2Fp%2F", "%3"); + console.log("Photosphere ID extracted: ", id); + getSphere(id); + } else { + ID("infoStatus").textContent = "Error: Could not understand link."; + } +} + +// +// PATH OBTAINING LOGIC +// + +function getPath(panoid) { + ID("infoStatus").textContent = "Getting data from server..."; + ID("infoID").textContent = panoid; + ID("infoType").textContent = `Street View`; + var request = new XMLHttpRequest(); + request.onload = function () { + ID("infoStatus").textContent = "Parsing data..."; + // Metadata parsing witchcraft + const data = JSON.parse(this.responseText.substring(4))[1][0]; + + const tileSize = data[2][3][1][0]; + const bestZoom = (data[2][3][0].length - 1); + + if (bestZoom < zoom || zoom < 0) { + zoom = bestZoom; + document.getElementById("zoom").value = bestZoom; + } + + const copyright = data[4][0][0][0][0]; + + ID("infoZooms").textContent = ` 0 to ${bestZoom}`; + ID("infoCopyright").textContent = copyright; + + const widthPX = data[2][3][0][zoom][0][1]; + const heightPX = data[2][3][0][zoom][0][0]; + const width = Math.ceil(widthPX / tileSize); + const height = Math.ceil(heightPX / tileSize); + + prepCanvas(widthPX, heightPX); + + ID("infoStatus").textContent = "Downloading tiles"; + const totalTiles = width * height; + var processedTiles = 0; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + requestImage(`https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid=${panoid}&x=${x}&y=${y}&zoom=${zoom}&nbt=1&fover=0`, (image) => { + paintImage(image, x, y, heightPX, widthPX, tileSize, height); + processedTiles++; + if (processedTiles === totalTiles) { + ID("infoStatus").textContent = "finished"; + } + }); + } + } + }; + request.open("get", `https://www.google.com/maps/photometa/v1?authuser=0&pb=!1m4!1smaps_sv.tactile!11m2!2m1!1b1!2m2!1sen!2sus!3m3!1m2!1e2!2s${panoid}!4m57!1e1!1e2!1e3!1e4!1e5!1e6!1e8!1e12!2m1!1e1!4m1!1i48!5m1!1e1!5m1!1e2!6m1!1e1!6m1!1e2!9m36!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e1!2b0!3e3!1m3!1e4!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e3`, true); + request.send(); + +} + +// +// PHOTOSHPERE OBTAINING LOGIC +// + +function getSphere(sphereID) { + ID("infoStatus").textContent = "Getting data from server..."; + ID("infoID").textContent = sphereID; + ID("infoType").textContent = `Photo Sphere`; + var request = new XMLHttpRequest(); + request.onload = function () { + ID("infoStatus").textContent = "Parsing data..."; + // Metadata parsing witchcraft + const data = JSON.parse(this.responseText.substring(4))[1][0]; + const tileSize = data[2][3][1][0]; + const bestZoom = (data[2][3][0].length - 1); + + if (bestZoom < zoom || zoom < 0) { + zoom = bestZoom; + document.getElementById("zoom").value = bestZoom; + } + + const widthPX = data[2][3][0][zoom][0][1]; + const heightPX = data[2][3][0][zoom][0][0]; + const width = Math.ceil(widthPX / tileSize); + const height = Math.ceil(heightPX / tileSize); + + prepCanvas(widthPX, heightPX); + + const copyright = data[4][1][0][0]; + ID("infoZooms").textContent = bestZoom; + ID("infoCopyright").textContent = copyright; + + prepCanvas(widthPX, heightPX); + + ID("infoStatus").textContent = "Downloading tiles"; + const totalTiles = width * height; + var processedTiles = 0; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + requestImage(`https://lh3.ggpht.com/p/${sphereID}=x${x}-y${y}-z${zoom}`, (image) => { + paintImage(image, x, y, heightPX, widthPX, tileSize, height); + processedTiles++; + if (processedTiles === totalTiles) { + ID("infoStatus").textContent = "finished"; + } + }); + } + } + }; + request.open("get", `https://www.google.com/maps/photometa/v1?authuser=0&hl=en&gl=us&pb=!1m4!1smaps_sv.tactile!11m2!2m1!1b1!2m2!1sen!2sus!3m3!1m2!1e10!2s${sphereID}!4m57!1e1!1e2!1e3!1e4!1e5!1e6!1e8!1e12!2m1!1e1!4m1!1i48!5m1!1e1!5m1!1e2!6m1!1e1!6m1!1e2!9m36!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e1!2b0!3e3!1m3!1e4!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e3`, true); + request.send(); + +} + +function requestImage(url, callback) { + var req = new XMLHttpRequest(); + req.responseType = "arraybuffer"; + req.onload = function () { + const blob = new Blob([this.response], { type: 'application/octet-binary' }); + var url = window.URL.createObjectURL(blob); + const image = new Image(); + image.src = url; + image.onload = () => { + callback(image); + }; + }; + req.open("get", url, true); + req.send(); +} + +function prepCanvas(widthPX, heightPX) { + canvas.width = widthPX; + canvas2.width = widthPX; + + if (heightPX > maxCanvasHeight) { + canvas2.height = heightPX / 2; + canvas.height = heightPX / 2; + } else { + canvas2.height = 0; + canvas2.width = 0; + canvas.height = heightPX; + } +} + +function paintImage(img, x, y, heightPX, widthPX, tileSize, height) { + if (heightPX > maxCanvasHeight) { + if ((y > ((height / 2) - 1)) && height !== 1) { + ctx2.drawImage(img, x * tileSize, y * tileSize - (heightPX / 2)); + } else { + ctx.drawImage(img, x * tileSize, y * tileSize); + } + } else { + ctx.drawImage(img, x * tileSize, y * tileSize); + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..f070d10 --- /dev/null +++ b/style.css @@ -0,0 +1,127 @@ +canvas { + /* border: 1px ridge white; */ + margin: 10px; + max-width: 90%; + margin-top: 0px; + margin-bottom: 0px; + border-bottom: none; + border-top: none; + display: block; + margin-left: auto; + margin-right: auto; +} + +header { + margin: 0px; +} + +header h1, +header h2, +header h3, +header h4, +header h5, +header h6 { + color: white; + text-shadow: 3px 8px #111111; + text-align: center; + letter-spacing: 2px; + font-family: Georgia, serif; + margin: 0px; + padding: 0px; +} + +header h1 { + font-size: 50px; + text-decoration: underline; +} + +header h2 { + font-size: 40px; +} + +section { + background-color: #36393c; + border-top: 5px solid white; + border-bottom: 5px solid white; + color: white; + border-radius: 5px; + margin-bottom: 25px; + padding-left: 25px; + padding-right: 25px; +} + +footer { + text-align: center; + font-size: x-large; +} + +input[type=button] { + width: 100%; + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; +} + +input[type=text] { + width: 100%; + padding: 12px 20px; + margin: 3px 0; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + background-color: lightgray; +} + +input[type=number], +select { + width: 100%; + padding: 12px 20px; + margin: 3px 0; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + background-color: lightgray; +} + +input.tall { + height: 100px; + font-size: 2em; +} + +body { + background: #2c2f33; + color: white; + font-family: "Arial"; +} + +main { + margin-top: 0px; + margin-left: auto; + margin-right: auto; + max-width: 80%; +} + +main h1, +main h2, +main h3, +main h4, +main h5, +main h6 { + color: white; + text-align: center; + margin: 0px; + padding: 0px; + letter-spacing: 2px; +} + +main h1 { + font-size: 50px; +} + +main h2 { + font-size: 40px; +} \ No newline at end of file