Add new ui and refactor code
This commit is contained in:
parent
76e508ee96
commit
f02371d2d4
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
# [info](https://alexanderbass.com/programming/gsvviewer/)
|
# [info](https://alexanderbass.com/programming/gsvviewer/)
|
||||||
|
|
||||||
Contributions and bug reports are welcome. This gitea page does not allow any user signups so please email me any contributions/issues. My email adress can be found at [my website](https://alexanderbass.com/info/)
|
Contributions and bug reports are welcome. This git page does not allow any user signups so please email me any contributions/issues. My email adress can be found at [my website](https://alexanderbass.com/info/)
|
188
index.html
188
index.html
|
@ -5,6 +5,148 @@
|
||||||
<title>GSV image viewer</title>
|
<title>GSV image viewer</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
|
<style>
|
||||||
|
#infoPanel{
|
||||||
|
border: 5px solid white;
|
||||||
|
}
|
||||||
|
.selectorBox{
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid gray;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
#statusBox{
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#statusBox > progress {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.selectorBox:empty {
|
||||||
|
display: none;}
|
||||||
|
.selectorBox > span {
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
.selected{
|
||||||
|
border: 2px solid white;
|
||||||
|
background-color: gray;
|
||||||
|
|
||||||
|
}
|
||||||
|
input:disabled{
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
.gsvFlex{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gsvFlexLeft{
|
||||||
|
flex:5;
|
||||||
|
}
|
||||||
|
.gsvFlexRight{
|
||||||
|
flex:0.75;
|
||||||
|
margin-block: 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding-inline: 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
#infoOutput{
|
||||||
|
border: 2px solid gray;
|
||||||
|
}
|
||||||
|
.gsvFlexRight:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nearbyLocation {
|
||||||
|
border: 2px solid gray;
|
||||||
|
padding: 4px;
|
||||||
|
margin:8px;
|
||||||
|
}
|
||||||
|
#yearSelector {
|
||||||
|
border: 2px solid gray;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
columns: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 200px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
#currentYear, .yearButton {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#currentYear {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
.yearButton{
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
border:2px solid transparent;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.yearButton:hover {
|
||||||
|
border:2px solid black;
|
||||||
|
}
|
||||||
|
#nearbyLocations {
|
||||||
|
display:flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#compass{
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: f;
|
||||||
|
height: 300px;
|
||||||
|
width: 300px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
#compass > .guide {
|
||||||
|
position: relative;
|
||||||
|
width: 1%;
|
||||||
|
height: 1%;
|
||||||
|
z-index: 9;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
#compass>.guide#vert {
|
||||||
|
width: 100%;
|
||||||
|
top:50%;
|
||||||
|
|
||||||
|
}
|
||||||
|
#compass>.guide#horz {
|
||||||
|
height: 100%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
#compass>#pointContainer {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
top: 0%;
|
||||||
|
transform: translate(0,-100%);
|
||||||
|
}
|
||||||
|
#pointContainer {
|
||||||
|
background-color: wheat;
|
||||||
|
border-radius: 500px;
|
||||||
|
}
|
||||||
|
#pointContainer>.point {
|
||||||
|
position: absolute;
|
||||||
|
width: 3%;
|
||||||
|
height: 3%;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
#pointContainer>.point:hover {
|
||||||
|
z-index: 10;
|
||||||
|
background-color: orange;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#experimental {
|
||||||
|
margin-block: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
@ -18,28 +160,36 @@
|
||||||
<p>
|
<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.
|
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>
|
</p>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="upperCanvas"></canvas>
|
||||||
<canvas id="canvas2"></canvas>
|
<canvas id="lowerCanvas"></canvas>
|
||||||
|
|
||||||
<hr>
|
<input type="text" placeholder="Input valid link to google streetview or google photoshpere page" name="" id="downloadURL" value="">
|
||||||
|
<div id="qualitySelector" class="selectorBox"></div>
|
||||||
<div id="infoPanel">
|
<div id="statusBox" style="visibility: hidden;">
|
||||||
<b>Status: </b><span id="infoStatus">N/A</span> |
|
<progress id="downloadProgress" max="100" value="0"></progress></div>
|
||||||
<b>Image Type: </b><span id="infoType">N/A</span> |
|
<div class="gsvFlex">
|
||||||
<b>Image ID: </b><span id="infoID">N/A</span> |
|
<div class="gsvFlexLeft">
|
||||||
<b>Zoom Levels: </b><span id="infoZooms">N/A</span> |
|
<input type="button" value="Load Image" id="loadButton">
|
||||||
<b>Copyright: </b><span id="infoCopyright">N/A</span>
|
<input type="button" value="Download Image" id="downloadButton">
|
||||||
|
</div>
|
||||||
|
<div class="gsvFlexRight" id="infoOutput"></div>
|
||||||
|
</div>
|
||||||
|
<details id="experimentalExpand">
|
||||||
|
<summary id="experimental">Experimental Features</summary>
|
||||||
|
<div class="gsvFlex">
|
||||||
|
<div class="gsvFlexLeft">
|
||||||
|
Compass
|
||||||
|
<div id="compass">
|
||||||
|
<div class="guide" id="vert"></div>
|
||||||
|
<div class="guide" id="horz"></div>
|
||||||
|
<div id="pointContainer"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<div id="gsvFlexRight"></div>
|
||||||
|
Year Selector
|
||||||
<h2>Controls</h2>
|
<div id="yearSelector"></div>
|
||||||
<h3>Input URL</h3>
|
</details>
|
||||||
<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.
|
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.
|
||||||
|
|
||||||
|
|
551
index.js
551
index.js
|
@ -4,13 +4,14 @@
|
||||||
// VARIABLES
|
// VARIABLES
|
||||||
//
|
//
|
||||||
|
|
||||||
var canvas, canvas2, ctx, ctx2, zoom;
|
var zoom = 0;
|
||||||
const maxCanvasHeight = 7000; //px
|
|
||||||
var downloadCount = 0;
|
var downloadCount = 0;
|
||||||
|
var handler;
|
||||||
//
|
var infoDisplay;
|
||||||
// UTILITY FUNCTIONS
|
var statusDisplay;
|
||||||
//
|
var previousId;
|
||||||
|
var loadButton;
|
||||||
|
var downloadButton;
|
||||||
|
|
||||||
function ID(id) {
|
function ID(id) {
|
||||||
return document.getElementById(id);
|
return document.getElementById(id);
|
||||||
|
@ -28,226 +29,396 @@ function extractTextBetween(input, before, after) {
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
const urlBox = ID("downloadURL");
|
const urlBox = ID("downloadURL");
|
||||||
canvas = ID("canvas");
|
handler = new CanvasHandler(ID("lowerCanvas"), ID("upperCanvas"));
|
||||||
canvas2 = ID("canvas2");
|
|
||||||
ctx = canvas.getContext("2d");
|
|
||||||
ctx2 = canvas2.getContext("2d");
|
|
||||||
|
|
||||||
ID("loadButton").addEventListener("click", () => {
|
{
|
||||||
zoom = ID("zoom").value;
|
infoDisplay = ID("infoOutput");
|
||||||
console.log("URL : ", urlBox.value);
|
infoDisplay.clear = function () {
|
||||||
parseURL(urlBox.value);
|
this.innerHTML = "";
|
||||||
});
|
};
|
||||||
ID("downloadButton").addEventListener("click", () => {
|
infoDisplay.setInfo = function (property, value) {
|
||||||
downloadCanvas();
|
let el;
|
||||||
|
Array.from(this.childNodes).forEach((e) => {
|
||||||
|
if (e.id == property) {
|
||||||
|
el = e;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (el === undefined) {
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = property;
|
||||||
|
this.appendChild(el);
|
||||||
|
}
|
||||||
|
el.textContent = `${property}: ${value}`;
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
statusDisplay = ID("statusBox");
|
||||||
|
statusDisplay.downloadProgress = ID("downloadProgress");
|
||||||
|
statusDisplay.setProgress = function (done, of) {
|
||||||
|
const percent = 100 * done / of;
|
||||||
|
this.downloadProgress.value = percent;
|
||||||
|
this.downloadProgress.textContent = `${percent}%`;
|
||||||
|
};
|
||||||
|
statusDisplay.show = function () {
|
||||||
|
this.style.visibility = "inherit";
|
||||||
|
};
|
||||||
|
statusDisplay.hide = function () {
|
||||||
|
this.style.visibility = "hidden";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
loadButton = ID("loadButton");
|
||||||
|
downloadButton = ID("downloadButton");
|
||||||
|
downloadButton.disabled = true;
|
||||||
|
|
||||||
|
|
||||||
|
loadButton.addEventListener("click", () => {
|
||||||
|
let [func, id] = parseURL(urlBox.value);
|
||||||
|
new func(id).load();
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
downloadButton.addEventListener("click", () => {
|
||||||
//
|
handler.download();
|
||||||
//
|
});
|
||||||
|
|
||||||
function downloadCanvas() {
|
urlBox.addEventListener("input", () => {
|
||||||
ID("infoStatus").textContent = "saving image..";
|
checkMetadata();
|
||||||
if (canvas2.height > 0) {
|
});
|
||||||
const link1 = document.createElement('a');
|
checkMetadata();
|
||||||
link1.download = `top${downloadCount}.png`;
|
});
|
||||||
link1.href = canvas.toDataURL();
|
|
||||||
link1.click();
|
function checkMetadata() {
|
||||||
const link2 = document.createElement('a');
|
const dlUrl = ID("downloadURL");
|
||||||
link2.download = `bottom${downloadCount}.png`;
|
|
||||||
link2.href = canvas2.toDataURL();
|
|
||||||
link2.click();
|
const [cls, id] = parseURL(dlUrl.value);
|
||||||
downloadCount++;
|
|
||||||
|
if (cls === false) {
|
||||||
|
loadButton.disabled = true;
|
||||||
|
dlUrl.style.border = "2px solid red";
|
||||||
|
infoDisplay.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loadButton === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previousId = id;
|
||||||
|
if (cls !== false) {
|
||||||
|
const handle = new cls(id);
|
||||||
|
handle.getMetadata().then((metadata) => {
|
||||||
|
if (metadata === false) {
|
||||||
|
loadButton.disabled = true;
|
||||||
|
dlUrl.style.border = "2px solid red";
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
const link = document.createElement('a');
|
updateQualityRange(metadata.zooms);
|
||||||
link.download = `image${downloadCount}.png`;
|
}
|
||||||
link.href = canvas.toDataURL();
|
});
|
||||||
link.click();
|
|
||||||
downloadCount++;
|
dlUrl.style.border = "2px solid green";
|
||||||
|
loadButton.removeAttribute("disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetInfoDisplay() {
|
function updateQualityRange(zooms) {
|
||||||
const normal = "N/A";
|
const qualityContainer = ID("qualitySelector");
|
||||||
ID("infoStatus").textContent = normal;
|
qualityContainer.innerHTML = "";
|
||||||
ID("infoType").textContent = normal;
|
let qualityElements = [];
|
||||||
ID("infoCopyright").textContent = normal;
|
for (let i = 0; i < zooms.length; i++) {
|
||||||
ID("infoZooms").textContent = normal;
|
const span = document.createElement("span");
|
||||||
ID("infoID").textContent = normal;
|
const zoomLevel = zooms[i];
|
||||||
|
span.order = i;
|
||||||
|
span.textContent = `${i + 1}: ${zoomLevel[0]}x${zoomLevel[1]}`;
|
||||||
|
span.select = function () {
|
||||||
|
deselectAll();
|
||||||
|
this.classList.add("selected");
|
||||||
|
zoom = this.order;
|
||||||
|
};
|
||||||
|
span.deselect = function () {
|
||||||
|
this.classList.remove("selected");
|
||||||
|
};
|
||||||
|
span.addEventListener("click", (e) => {
|
||||||
|
e.target.select();
|
||||||
|
});
|
||||||
|
qualityElements.push(span);
|
||||||
|
qualityContainer.appendChild(span);
|
||||||
}
|
}
|
||||||
|
qualityElements[0].select();
|
||||||
|
|
||||||
|
function deselectAll() {
|
||||||
|
for (const e of qualityElements) {
|
||||||
|
e.deselect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function parseURL(url) {
|
function parseURL(url) {
|
||||||
resetInfoDisplay();
|
|
||||||
if (url.includes("panoid")) {
|
if (url.includes("panoid")) {
|
||||||
const id = extractTextBetween(url, "panoid%3D", "%");
|
const id = extractTextBetween(url, "panoid%3D", "%");
|
||||||
console.log("Extracted PanoID using method 1: ", id);
|
|
||||||
|
|
||||||
getPath(id);
|
return [GSVPath, id];
|
||||||
} else if (url.includes("m4!1s") && url.includes("data=")) {
|
} else if (url.includes("m4!1s") && url.includes("data=")) {
|
||||||
const id = extractTextBetween(url, "m4!1s", "!2e");
|
const id = extractTextBetween(url, "m4!1s", "!2e");
|
||||||
if (id.length > 22) {
|
if (id.length > 22) {
|
||||||
console.log("Extracted Photosphere ID using method 2: ", id);
|
return [GSVSphere, id];
|
||||||
getSphere(id);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Extracted PanoID using method 2: ", id);
|
return [GSVPath, id];
|
||||||
getPath(id);
|
|
||||||
}
|
}
|
||||||
} else if (url.includes("googleusercontent.com")) {
|
} else if (url.includes("googleusercontent.com")) {
|
||||||
const id = extractTextBetween(url, "googleusercontent.com%2Fp%2F", "%3");
|
const id = extractTextBetween(url, "googleusercontent.com%2Fp%2F", "%3");
|
||||||
console.log("Photosphere ID extracted: ", id);
|
return [GSVSphere, id];
|
||||||
getSphere(id);
|
|
||||||
} else {
|
} else {
|
||||||
ID("infoStatus").textContent = "Error: Could not understand link.";
|
return [false];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// PATH OBTAINING LOGIC
|
|
||||||
//
|
|
||||||
|
|
||||||
function getPath(panoid) {
|
class CanvasHandler {
|
||||||
ID("infoStatus").textContent = "Getting data from server...";
|
maxCanvasHeight = 7000;//pixels
|
||||||
ID("infoID").textContent = panoid;
|
doubleCanvas = false;
|
||||||
ID("infoType").textContent = `Street View`;
|
constructor(upperCanvas, lowerCanvas) {
|
||||||
var request = new XMLHttpRequest();
|
this.upper = upperCanvas;
|
||||||
request.onload = function () {
|
this.lower = lowerCanvas;
|
||||||
ID("infoStatus").textContent = "Parsing data...";
|
this.upperContext = upperCanvas.getContext("2d");
|
||||||
// Metadata parsing witchcraft
|
this.lowerContext = lowerCanvas.getContext("2d");
|
||||||
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];
|
setSize(widthPX, heightPX, tileSize) {
|
||||||
|
this.upper.width = widthPX;
|
||||||
|
this.lower.width = widthPX;
|
||||||
|
this.tileSize = tileSize;
|
||||||
|
|
||||||
ID("infoZooms").textContent = ` 0 to ${bestZoom}`;
|
if (heightPX > this.maxCanvasHeight) {
|
||||||
ID("infoCopyright").textContent = copyright;
|
this.lower.height = heightPX / 2;
|
||||||
|
this.upper.height = heightPX / 2;
|
||||||
const widthPX = data[2][3][0][zoom][0][1];
|
this.doubleCanvas = true;
|
||||||
const heightPX = data[2][3][0][zoom][0][0];
|
this.lower.style = "";
|
||||||
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 {
|
} else {
|
||||||
canvas2.height = 0;
|
this.doubleCanvas = false;
|
||||||
canvas2.width = 0;
|
this.lower.style = "display:none;";
|
||||||
canvas.height = heightPX;
|
this.upper.height = heightPX;
|
||||||
}
|
}
|
||||||
|
this.upperContext = this.upper.getContext("2d");
|
||||||
|
this.lowerContext = this.lower.getContext("2d");
|
||||||
}
|
}
|
||||||
|
paintImage(img, x, y, heightPX, widthPX, height) {
|
||||||
function paintImage(img, x, y, heightPX, widthPX, tileSize, height) {
|
if (this.doubleCanvas) {
|
||||||
if (heightPX > maxCanvasHeight) {
|
|
||||||
if ((y > ((height / 2) - 1)) && height !== 1) {
|
if ((y > ((height / 2) - 1)) && height !== 1) {
|
||||||
ctx2.drawImage(img, x * tileSize, y * tileSize - (heightPX / 2));
|
this.upperContext.drawImage(img, x * this.tileSize, y * this.tileSize - (heightPX / 2));
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(img, x * tileSize, y * tileSize);
|
this.lowerContext.drawImage(img, x * this.tileSize, y * this.tileSize);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(img, x * tileSize, y * tileSize);
|
this.upperContext.drawImage(img, x * this.tileSize, y * this.tileSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
download() {
|
||||||
|
if (this.doubleCanvas) {
|
||||||
|
const link2 = document.createElement('a');
|
||||||
|
link2.download = `bottom${downloadCount}.png`;
|
||||||
|
link2.href = this.lower.toDataURL();
|
||||||
|
link2.click();
|
||||||
|
downloadCount++;
|
||||||
|
}
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `image${downloadCount}.png`;
|
||||||
|
link.href = this.upper.toDataURL();
|
||||||
|
link.click();
|
||||||
|
downloadCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
function angleToCompasPercentage(pos1, pos2) {
|
||||||
|
const [lat1, lon1] = pos1;
|
||||||
|
const [lat2, lon2] = pos2;
|
||||||
|
const [latDif, lonDif] = [lat2 - lat1, lon2 - lon1];
|
||||||
|
const angle = Math.atan2(latDif, lonDif);
|
||||||
|
const x = (Math.cos(angle) * 48 + 48);
|
||||||
|
const y = (-Math.sin(angle) * 48 + 48);
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PanoDownloader {
|
||||||
|
|
||||||
|
constructor(id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.meta = await this.getMetadata();
|
||||||
|
infoDisplay.setInfo("Author", this.meta.copyright);
|
||||||
|
// infoDisplay.setInfo("Location", `${this.meta.lat}, ${this.meta.lon}`);
|
||||||
|
this.#plot();
|
||||||
|
|
||||||
|
if ("nearme" in this.meta === true && experimental) {
|
||||||
|
this.experimentalFeatures();
|
||||||
|
ID("experimentalExpand").style.display = "block";
|
||||||
|
} else {
|
||||||
|
ID("experimentalExpand").style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
experimentalFeatures() {
|
||||||
|
const yearSelector = ID("yearSelector");
|
||||||
|
const container = ID("pointContainer");
|
||||||
|
yearSelector.innerHTML = "";
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
{
|
||||||
|
let el = document.createElement("span");
|
||||||
|
el.textContent = MONTHS[this.meta.date[1]] + " " + this.meta.date[0];
|
||||||
|
el.id = "currentYear";
|
||||||
|
yearSelector.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
const locations = this.meta.nearme.map((a) => a[0]);
|
||||||
|
for (let i = 1; i < locations.length; i++) {
|
||||||
|
if (this.meta.nearme[i][2] === undefined) {
|
||||||
|
let [x, y] = angleToCompasPercentage([this.meta.lat, this.meta.lon], this.meta.nearme[i][1]);
|
||||||
|
|
||||||
|
let el = document.createElement("div");
|
||||||
|
el.classList.add("point");
|
||||||
|
el.style.left = `${x}%`;
|
||||||
|
el.style.top = `${y}%`;
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
|
new (Object.getPrototypeOf(this).constructor)(locations[i]).load();
|
||||||
|
});
|
||||||
|
container.appendChild(el);
|
||||||
|
} else {
|
||||||
|
let el = document.createElement("span");
|
||||||
|
el.classList.add("yearButton");
|
||||||
|
el.textContent = MONTHS[this.meta.nearme[i][2][1]] + " " + this.meta.nearme[i][2][0];
|
||||||
|
if (this.meta.date === this.meta.nearme[i][2]) {
|
||||||
|
}
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
|
new (Object.getPrototypeOf(this).constructor)(locations[i]).load();
|
||||||
|
});
|
||||||
|
yearSelector.appendChild(el);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata() {
|
||||||
|
infoDisplay.clear();
|
||||||
|
infoDisplay.setInfo("Image ID", this.id);
|
||||||
|
|
||||||
|
return fetch(this.formatMetadataURL()).then((response) => response.text())
|
||||||
|
.then((responseText) => {
|
||||||
|
return this.extractMetadata(responseText);
|
||||||
|
}).catch((err) => { console.error(err); return false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
async #plot() {
|
||||||
|
loadButton.disabled = true;
|
||||||
|
downloadButton.disabled = true;
|
||||||
|
statusDisplay.show();
|
||||||
|
statusDisplay.setProgress(0, 1);
|
||||||
|
handler.setSize(this.meta.widthPX, this.meta.heightPX, this.meta.tileSize);
|
||||||
|
|
||||||
|
const totalTiles = this.meta.width * this.meta.height;
|
||||||
|
var processedTiles = 0;
|
||||||
|
|
||||||
|
for (let x = 0; x < this.meta.width; x++) {
|
||||||
|
for (let y = 0; y < this.meta.height; y++) {
|
||||||
|
this.#downloadImage(this.formatImageUrl(x, y, zoom)).then((image) => {
|
||||||
|
handler.paintImage(image, x, y, this.meta.heightPX, this.meta.widthPX, this.meta.height);
|
||||||
|
processedTiles++;
|
||||||
|
statusDisplay.setProgress(processedTiles, totalTiles);
|
||||||
|
if (processedTiles === totalTiles) {
|
||||||
|
loadButton.disabled = false;
|
||||||
|
downloadButton.disabled = false;
|
||||||
|
statusDisplay.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async #downloadImage(url) {
|
||||||
|
return fetch(url).then(response => response.blob()).then(imageBlob => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const imageObjectURL = URL.createObjectURL(imageBlob);
|
||||||
|
const image = new Image();
|
||||||
|
image.src = imageObjectURL;
|
||||||
|
image.onload = () => {
|
||||||
|
resolve(image);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GSVPath extends PanoDownloader {
|
||||||
|
experimental = true;
|
||||||
|
formatMetadataURL() {
|
||||||
|
return (
|
||||||
|
"https://www.google.com/maps/photometa/v1?authuser=0&pb=!1m4!1smaps_sv.tactile" +
|
||||||
|
"!11m2!2m1!1b1!2m2!1sen!2sus!3m3!1m2!1e2!2s" +
|
||||||
|
this.id +
|
||||||
|
"!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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
formatImageUrl(x, y, zoom) {
|
||||||
|
return `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid=${this.id}&x=${x}&y=${y}&zoom=${zoom}&nbt=1&fover=0`;
|
||||||
|
}
|
||||||
|
extractMetadata(responseText) {
|
||||||
|
let meta = {};
|
||||||
|
const data = JSON.parse(responseText.substring(4))[1][0];
|
||||||
|
meta.nearme = data[5]?.[0]?.[3]?.[0].map((a) => [a?.[0]?.[1], [a?.[2]?.[0]?.[2], a?.[2]?.[0]?.[3]], undefined]);
|
||||||
|
data?.[5]?.[0]?.[8]?.forEach((a) => meta.nearme[a[0]][2] = a?.[1]);
|
||||||
|
|
||||||
|
meta.tileSize = data[2][3]?.[1]?.[0];
|
||||||
|
meta.zooms = data[2][3][0].map((a) => a[0]).map((b) => [b[1], b[0]]);
|
||||||
|
meta.widthPX = meta.zooms[zoom][0];
|
||||||
|
meta.lat = data[5][0][1][0][2];
|
||||||
|
meta.lon = data[5][0][1][0][3];
|
||||||
|
meta.heightPX = meta.zooms[zoom][1];
|
||||||
|
meta.width = Math.ceil(meta.widthPX / meta.tileSize);
|
||||||
|
meta.height = Math.ceil(meta.heightPX / meta.tileSize);
|
||||||
|
meta.date = data[6][7];
|
||||||
|
// meta.address = `${data[3]?.[2]?.[0]?.[0]} ${data[3]?.[2]?.[1]?.[0]}`;
|
||||||
|
meta.copyright = data[4][0][0][0][0];
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GSVSphere extends PanoDownloader {
|
||||||
|
formatMetadataURL() {
|
||||||
|
return (
|
||||||
|
"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" +
|
||||||
|
this.id +
|
||||||
|
"!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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
extractMetadata(responseText) {
|
||||||
|
let meta = {};
|
||||||
|
const data = JSON.parse(responseText.substring(4))[1][0];
|
||||||
|
meta.tileSize = data[2][3][1][0];
|
||||||
|
meta.bestZoom = (data[2][3][0].length - 1);
|
||||||
|
meta.zooms = data[2][3][0].map((a) => a[0]).map((b) => [b[1], b[0]]);
|
||||||
|
|
||||||
|
meta.widthPX = meta.zooms[zoom][0];
|
||||||
|
meta.heightPX = meta.zooms[zoom][1];
|
||||||
|
meta.width = Math.ceil(meta.widthPX / meta.tileSize);
|
||||||
|
meta.height = Math.ceil(meta.heightPX / meta.tileSize);
|
||||||
|
|
||||||
|
meta.copyright = data[4][1][0][0];
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
formatImageUrl(x, y, zoom) {
|
||||||
|
return `https://lh3.ggpht.com/p/${this.id}=x${x}-y${y}-z${zoom}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue