UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

338 lines (301 loc) 9.85 kB
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; }