2022-09-24 02:11:44 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
//
|
|
|
|
// VARIABLES
|
|
|
|
//
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
var zoom = 0;
|
2022-09-24 02:11:44 +00:00
|
|
|
var downloadCount = 0;
|
2023-04-03 17:46:38 +00:00
|
|
|
var handler;
|
|
|
|
var infoDisplay;
|
|
|
|
var statusDisplay;
|
|
|
|
var previousId;
|
|
|
|
var loadButton;
|
|
|
|
var downloadButton;
|
2022-09-24 02:11:44 +00:00
|
|
|
|
|
|
|
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");
|
2023-04-03 17:46:38 +00:00
|
|
|
handler = new CanvasHandler(ID("lowerCanvas"), ID("upperCanvas"));
|
|
|
|
|
|
|
|
{
|
|
|
|
infoDisplay = ID("infoOutput");
|
|
|
|
infoDisplay.clear = function () {
|
|
|
|
this.innerHTML = "";
|
|
|
|
};
|
|
|
|
infoDisplay.setInfo = function (property, value) {
|
|
|
|
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();
|
2022-09-24 02:11:44 +00:00
|
|
|
});
|
2023-04-03 17:46:38 +00:00
|
|
|
|
|
|
|
urlBox.addEventListener("input", () => {
|
|
|
|
checkMetadata();
|
2022-09-24 02:11:44 +00:00
|
|
|
});
|
2023-04-03 17:46:38 +00:00
|
|
|
checkMetadata();
|
2022-09-24 02:11:44 +00:00
|
|
|
});
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
function checkMetadata() {
|
|
|
|
const dlUrl = ID("downloadURL");
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
|
|
|
|
const [cls, id] = parseURL(dlUrl.value);
|
|
|
|
|
|
|
|
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 {
|
|
|
|
updateQualityRange(metadata.zooms);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
dlUrl.style.border = "2px solid green";
|
|
|
|
loadButton.removeAttribute("disabled");
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
function updateQualityRange(zooms) {
|
|
|
|
const qualityContainer = ID("qualitySelector");
|
|
|
|
qualityContainer.innerHTML = "";
|
|
|
|
let qualityElements = [];
|
|
|
|
for (let i = 0; i < zooms.length; i++) {
|
|
|
|
const span = document.createElement("span");
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
|
|
|
|
|
2022-09-24 02:11:44 +00:00
|
|
|
function parseURL(url) {
|
|
|
|
if (url.includes("panoid")) {
|
|
|
|
const id = extractTextBetween(url, "panoid%3D", "%");
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
return [GSVPath, id];
|
2022-09-24 02:11:44 +00:00
|
|
|
} else if (url.includes("m4!1s") && url.includes("data=")) {
|
|
|
|
const id = extractTextBetween(url, "m4!1s", "!2e");
|
|
|
|
if (id.length > 22) {
|
2023-04-03 17:46:38 +00:00
|
|
|
return [GSVSphere, id];
|
2022-09-24 02:11:44 +00:00
|
|
|
} else {
|
2023-04-03 17:46:38 +00:00
|
|
|
return [GSVPath, id];
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
} else if (url.includes("googleusercontent.com")) {
|
|
|
|
const id = extractTextBetween(url, "googleusercontent.com%2Fp%2F", "%3");
|
2023-04-03 17:46:38 +00:00
|
|
|
return [GSVSphere, id];
|
2022-09-24 02:11:44 +00:00
|
|
|
} else {
|
2023-04-03 17:46:38 +00:00
|
|
|
return [false];
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
class CanvasHandler {
|
|
|
|
maxCanvasHeight = 7000;//pixels
|
|
|
|
doubleCanvas = false;
|
|
|
|
constructor(upperCanvas, lowerCanvas) {
|
|
|
|
this.upper = upperCanvas;
|
|
|
|
this.lower = lowerCanvas;
|
|
|
|
this.upperContext = upperCanvas.getContext("2d");
|
|
|
|
this.lowerContext = lowerCanvas.getContext("2d");
|
|
|
|
}
|
|
|
|
|
|
|
|
setSize(widthPX, heightPX, tileSize) {
|
|
|
|
this.upper.width = widthPX;
|
|
|
|
this.lower.width = widthPX;
|
|
|
|
this.tileSize = tileSize;
|
|
|
|
|
|
|
|
if (heightPX > this.maxCanvasHeight) {
|
|
|
|
this.lower.height = heightPX / 2;
|
|
|
|
this.upper.height = heightPX / 2;
|
|
|
|
this.doubleCanvas = true;
|
|
|
|
this.lower.style = "";
|
|
|
|
} else {
|
|
|
|
this.doubleCanvas = false;
|
|
|
|
this.lower.style = "display:none;";
|
|
|
|
this.upper.height = heightPX;
|
|
|
|
}
|
|
|
|
this.upperContext = this.upper.getContext("2d");
|
|
|
|
this.lowerContext = this.lower.getContext("2d");
|
|
|
|
}
|
|
|
|
paintImage(img, x, y, heightPX, widthPX, height) {
|
|
|
|
if (this.doubleCanvas) {
|
|
|
|
if ((y > ((height / 2) - 1)) && height !== 1) {
|
|
|
|
this.upperContext.drawImage(img, x * this.tileSize, y * this.tileSize - (heightPX / 2));
|
|
|
|
} else {
|
|
|
|
this.lowerContext.drawImage(img, x * this.tileSize, y * this.tileSize);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
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++;
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
2023-04-03 17:46:38 +00:00
|
|
|
const link = document.createElement('a');
|
|
|
|
link.download = `image${downloadCount}.png`;
|
|
|
|
link.href = this.upper.toDataURL();
|
|
|
|
link.click();
|
|
|
|
downloadCount++;
|
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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];
|
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
class PanoDownloader {
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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";
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
2023-04-03 17:46:38 +00:00
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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);
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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; });
|
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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);
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
const totalTiles = this.meta.width * this.meta.height;
|
2022-09-24 02:11:44 +00:00
|
|
|
var processedTiles = 0;
|
2023-04-03 17:46:38 +00:00
|
|
|
|
|
|
|
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);
|
2022-09-24 02:11:44 +00:00
|
|
|
processedTiles++;
|
2023-04-03 17:46:38 +00:00
|
|
|
statusDisplay.setProgress(processedTiles, totalTiles);
|
2022-09-24 02:11:44 +00:00
|
|
|
if (processedTiles === totalTiles) {
|
2023-04-03 17:46:38 +00:00
|
|
|
loadButton.disabled = false;
|
|
|
|
downloadButton.disabled = false;
|
|
|
|
statusDisplay.hide();
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
});
|
2023-04-03 17:46:38 +00:00
|
|
|
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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;
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 17:46:38 +00:00
|
|
|
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"
|
|
|
|
);
|
2022-09-24 02:11:44 +00:00
|
|
|
}
|
2023-04-03 17:46:38 +00:00
|
|
|
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}`;
|
|
|
|
}
|
|
|
|
}
|