Initial Commit
This commit is contained in:
commit
4962222901
52
index.html
Normal file
52
index.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>GSV image viewer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>GSV image viewer</h1>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
<canvas id="canvas2"></canvas>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div id="infoPanel">
|
||||||
|
<b>Status: </b><span id="infoStatus">N/A</span> |
|
||||||
|
<b>Image Type: </b><span id="infoType">N/A</span> |
|
||||||
|
<b>Image ID: </b><span id="infoID">N/A</span> |
|
||||||
|
<b>Zoom Levels: </b><span id="infoZooms">N/A</span> |
|
||||||
|
<b>Copyright: </b><span id="infoCopyright">N/A</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Controls</h2>
|
||||||
|
<h3>Input URL</h3>
|
||||||
|
<input type="text" placeholder="Input valid link to google streetview or google photoshpere page" name="" id="downloadURL" value="">
|
||||||
|
<input type="button" value="Load Image" id="loadButton">
|
||||||
|
<h3>Resolution Level</h3>
|
||||||
|
<input type="number" name="zoom" id="zoom" value=6>
|
||||||
|
<input type="button" value="Download Image" id="downloadButton">
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
This tool is not in any way affiliated or endorsed with or by google, google maps, or google street view. <br> Use at your own risk and respect for copyright
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
253
index.js
Normal file
253
index.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
127
style.css
Normal file
127
style.css
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue