@panoramax/web-viewer
Version:
Panoramax web viewer for geolocated pictures
338 lines (301 loc) • 9.85 kB
JavaScript
import { InternetFastTestFile, InternetFastThreshold } from "./services";
export const BASE_PANORAMA_ID = "geovisio-fake-id-0";
export const COLORS = {
BASE: "#FF6F00",
SELECTED: "#1E88E5",
HIDDEN: "#34495E",
NEXT: "#ffab40",
QUALI_1: "#00695C", // 360
QUALI_2: "#fd8d3c", // flat
PALETTE_1: "#fecc5c", // Oldest
PALETTE_2: "#fd8d3c",
PALETTE_3: "#f03b20",
PALETTE_4: "#bd0026" // Newest
};
export const COLORS_HEX = Object.fromEntries(Object.entries(COLORS).map(e => {
e[1] = parseInt(e[1].slice(1), 16);
return e;
}));
export const QUALITYSCORE_VALUES = [
{ color: "#007f4e", label: "A" },
{ color: "#72b043", label: "B" },
{ color: "#b5be2f", label: "C" },
{ color: "#f8cc1b", label: "D" },
{ color: "#f6a020", label: "E" },
];
export const QUALITYSCORE_RES_FLAT_VALUES = [1, 10, 2, 15, 3, 30, 4]; // Grade, < Px/FOV value
export const QUALITYSCORE_RES_360_VALUES = [3, 15, 4, 30, 5]; // Grade, < Px/FOV value
export const QUALITYSCORE_GPS_VALUES = [5, 1.01, 4, 2.01, 3, 5.01, 2, 10.01, 1]; // Grade, < Meters value
export const QUALITYSCORE_POND_RES = 4/5;
export const QUALITYSCORE_POND_GPS = 1/5;
/**
* Checks if a picture or sequence ID is kinda-null.
* @param {string|null|undefined} id The ID to check
* @returns True if null-like
*/
export function isNullId(id) {
return [null, undefined, "", BASE_PANORAMA_ID].includes(id);
}
/**
* Find the grade associated to an input Quality Score definition.
* @param {number[]} ranges The QUALITYSCORE_*_VALUES definition
* @param {number} value The picture value
* @return {number} The corresponding grade (1 to 5, or null if missing)
* @private
*/
export function getGrade(ranges, value) {
if(value === null || value === undefined || value === "") { return null; }
// Read each pair from table (grade, reference value)
for(let i = 0; i < ranges.length; i += 2) {
const grade = ranges[i];
const limit = ranges[i+1];
// Send grade if value is under limit
if (value < limit) { return grade;}
}
// Otherwise, send last grade
return ranges[ranges.length - 1];
}
/**
* Get cartesian distance between two points
* @param {number[]} from Start [x,y] coordinates
* @param {number[]} to End [x,y] coordinates
* @returns {number} The distance
* @private
*/
export function getDistance(from, to) {
const dx = from[0] - to[0];
const dy = from[1] - to[1];
return Math.sqrt(dx*dx + dy*dy);
}
/**
* Transforms a Base64 SVG string into a DOM img element.
* @param {string} svg The SVG as Base64 string
* @returns {Element} The DOM image element
* @private
*/
export function svgToPSVLink(svg, fillColor) {
try {
const svgStr = atob(svg.replace(/^data:image\/svg\+xml;base64,/, ""));
const svgXml = (new DOMParser()).parseFromString(svgStr, "image/svg+xml").childNodes[0];
const btn = document.createElement("button");
btn.appendChild(svgXml);
btn.classList.add("pnx-psv-tour-arrows", "pnx-print-hidden");
btn.style.color = fillColor;
return btn;
}
catch(e) {
const img = document.createElement("img");
img.src = svg;
img.alt = "";
return img;
}
}
/**
* Clones a model PSV link
* @private
*/
export function getArrow(a) {
const d = a.cloneNode(true);
d.addEventListener("pointerup", () => d.classList.add("pnx-clicked"));
return d;
}
/**
* Get direction based on angle
* @param {number[]} from Start [x,y] coordinates
* @param {number[]} to End [x,y] coordinates
* @returns {number} The azimuth, from 0 to 360°
* @private
*/
export function getAzimuth(from, to) {
return (Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI) + 360) % 360;
}
/**
* Computes relative heading for a single picture, based on its metadata
* @param {*} m The picture metadata
* @returns {number} The relative heading
* @private
*/
export function getRelativeHeading(m) {
if(!m) { throw new Error("No picture selected"); }
let prevSegDir, nextSegDir;
const currHeading = m.properties["view:azimuth"];
// Previous picture GPS coordinates
if(m?.sequence?.prevPic) {
const prevLink = m?.links?.find(l => l.nodeId === m.sequence.prevPic);
if(prevLink) {
prevSegDir = (((currHeading - getAzimuth(prevLink.gps, m.gps)) + 180) % 360) - 180;
}
}
// Next picture GPS coordinates
if(m?.sequence?.nextPic) {
const nextLink = m?.links?.find(l => l.nodeId === m.sequence.nextPic);
if(nextLink) {
nextSegDir = (((currHeading - getAzimuth(m.gps, nextLink.gps)) + 180) % 360) - 180;
}
}
return prevSegDir !== undefined ? prevSegDir : (nextSegDir !== undefined ? nextSegDir : 0);
}
/**
* Get direction based on angle
* @param {number[]} from Start [x,y] coordinates
* @param {number[]} to End [x,y] coordinates
* @returns {string} Direction (N/ENE/ESE/S/WSW/WNW)
* @private
*/
export function getSimplifiedAngle(from, to) {
const angle = Math.atan2(to[0] - from[0], to[1] - from[1]) * (180 / Math.PI); // -180 to 180°
// 6 directions version
if (Math.abs(angle) < 30) { return "N"; }
else if (angle >= 30 && angle < 90) { return "ENE"; }
else if (angle >= 90 && angle < 150) { return "ESE"; }
else if (Math.abs(angle) >= 150) { return "S"; }
else if (angle <= -30 && angle > -90) { return "WNW"; }
else if (angle <= -90 && angle > -150) { return "WSW"; }
}
/**
* Converts result from getPosition or position-updated event into x/y/z coordinates
*
* @param {object} pos pitch/yaw as given by PSV
* @param {number} zoom zoom as given by PSV
* @returns {object} Coordinates as x/y in degrees and zoom as given by PSV
* @private
*/
export function positionToXYZ(pos, zoom = undefined) {
const res = {
x: pos.yaw * (180/Math.PI),
y: pos.pitch * (180/Math.PI)
};
if(zoom !== undefined) { res.z = zoom; }
return res;
}
/**
* Converts x/y/z coordinates into PSV position (lat/lon/zoom)
*
* @param {number} x The X coordinate (in degrees)
* @param {number} y The Y coordinate (in degrees)
* @param {number} z The zoom level (0-100)
* @returns {object} Position coordinates as yaw/pitch/zoom
* @private
*/
export function xyzToPosition(x, y, z) {
return {
yaw: x / (180/Math.PI),
pitch: y / (180/Math.PI),
zoom: z
};
}
/**
* Transforms decimal degrees into degrees/minutes/seconds format.
* @param {number} degrees The decimal degrees value
* @returns {object} Coordinate as {d,m,s} object
*/
export function degToDms(degrees) {
const d = degrees < 0 ? Math.ceil(degrees) : Math.floor(degrees);
const rm = Math.abs(degrees - d) * 60;
const m = Math.floor(rm);
const s = parseFloat(((rm - m) * 60).toFixed(3));
return { d, m, s };
}
/**
* Get the query string for JOSM to load current picture area
* @returns {string} The query string, or null if not available
* @private
*/
export function josmBboxParameters(meta) {
if(meta) {
const coords = meta.gps;
const heading = meta?.properties?.["view:azimuth"];
const delta = 0.0002;
const values = {
left: coords[0] - (heading === null || heading >= 180 ? delta : 0),
right: coords[0] + (heading === null || heading <= 180 ? delta : 0),
top: coords[1] + (heading === null || heading <= 90 || heading >= 270 ? delta : 0),
bottom: coords[1] - (heading === null || (heading >= 90 && heading <= 270) ? delta : 0),
changeset_source: "Panoramax"
};
return Object.entries(values).map(e => e.join("=")).join("&");
}
else { return null; }
}
/**
* Check if code runs in an iframe or in a classic page.
* @returns {boolean} True if running in iframe
* @private
*/
export function isInIframe() {
try {
return window.self !== window.top;
} catch(e) {
return true;
}
}
const INTERNET_FAST_STORAGE = "pnx-internet-fast";
/**
* Check if Internet connection is high-speed or not.
* @returns {Promise} Resolves on true if high-speed.
* @private
*/
export function isInternetFast() {
// Check if downlink property is available
try {
const speed = navigator.connection.downlink; // MBit/s
return Promise.resolve(speed >= InternetFastThreshold());
}
// Fallback for other browsers
catch(e) {
try {
// Check if test has been done before and stored
const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
if(["true", "false"].includes(isFast)) {
return Promise.resolve(isFast === "true");
}
// Run download testing
const startTime = (new Date()).getTime();
return fetch(`${InternetFastTestFile()}?nocache=${startTime}`)
.then(async res => [res, await res.blob()])
.then(([res, blob]) => {
const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
const endTime = (new Date()).getTime();
const duration = (endTime - startTime) / 1000; // Transfer time in seconds
const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
const isFast = speed >= InternetFastThreshold();
sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
return isFast;
})
.catch(e => {
console.warn("Failed to run speedtest", e);
return false;
});
}
// Fallback for browser blocking third-party downloads or sessionStorage
catch(e) {
return Promise.resolve(false);
}
}
}
/**
* Get a cookie value
* @param {str} name The cookie name
* @returns {str} The cookie value, or null if not found
* @private
*/
export function getCookie(name) {
const parts = document.cookie
?.split(";")
?.find((row) => row.trimStart().startsWith(`${name}=`))
?.split("=");
if(!parts) { return undefined; }
parts.shift();
return parts.join("=");
}
/**
* Checks if an user account exists
* @returns {object} Object like {"id", "name"} or null if no authenticated account
* @private
*/
export function getUserAccount() {
const session = getCookie("session");
const user_id = getCookie("user_id");
const user_name = getCookie("user_name");
return (session && user_id && user_name) ? { id: user_id, name: user_name } : null;
}