UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

371 lines (320 loc) 11.5 kB
import { alterPSVState, MAP_FILTERS_JS2URL, alterMapState, alterViewerState, alterPhotoViewerState } from "./InitParameters"; // List of supported parameters const MANAGED_PARAMETERS = [ "speed", "nav", "focus", "pic", "xyz", "map", "background", "users", "pic_score", "s", "xywh", ].concat(Object.values(MAP_FILTERS_JS2URL)); // Events to listen on parent and PSV const UPDATE_PARENT_EVENTS = ["focus-changed", "pictures-navigation-changed"]; const UPDATE_PSV_EVENTS = ["position-updated", "zoom-updated", "view-rotated", "picture-loaded", "transition-duration-changed"]; const UPDATE_MAP_EVENTS = ["moveend", "zoomend", "boxzoomend", "background-changed", "users-changed", "filters-changed"]; /** * Updates the URL query part with various parent component information. * * Note that you may call `listenToChanges()` for this class to be effective once parent is ready-enough. * * @class Panoramax.utils.URLHandler * @typicalname urlHandler * @param {Panoramax.components.core.Basic} parent The parent component * @fires Panoramax.utils.URLHandler#url-changed */ export default class URLHandler extends EventTarget { constructor(parent) { super(); this._parent = parent; this._delay = null; } /** * Start listening to URL & parent changes through events. * This leads to parent & URL updates. * @memberof Panoramax.utils.URLHandler# */ listenToChanges() { window.addEventListener("popstate", this._onURLChange.bind(this), false); UPDATE_PARENT_EVENTS.forEach(e => this._parent.addEventListener(e, this._onParentChange.bind(this))); UPDATE_PSV_EVENTS.forEach(e => this._parent.psv.addEventListener(e, this._onParentChange.bind(this))); if(this._parent.map) { UPDATE_MAP_EVENTS.forEach(e => this._parent.map.on(e, this._onParentChange.bind(this))); } } /** * Call this function to stop listening to global events. * @memberof Panoramax.utils.URLHandler# */ destroy() { window.removeEventListener("popstate", this._onURLChange); } /** * Compute next values to insert in URL * @returns {object} Query parameters * @memberof Panoramax.utils.URLHandler# */ nextURLParams() { let hashParts = {}; if(typeof this._parent.psv.getTransitionDuration() == "number") { hashParts.speed = this._parent.psv.getTransitionDuration(); } if(![null, "any"].includes(this._parent.psv.getPicturesNavigation())) { hashParts.nav = this._parent.psv.getPicturesNavigation(); } if(this._parent.psv.getPictureId()) { hashParts.pic = this._parent.psv.getPictureId(); } const picMeta = this._parent.psv.getPictureMetadata(); if (picMeta) { hashParts.xyz = this.currentPSVString(); } if(this._parent.map) { hashParts.map = this.currentMapString(); hashParts.focus = "pic"; if(this._parent.isMapWide()) { hashParts.focus = "map"; } if(this._parent.map.hasTwoBackgrounds() && this._parent.map.getBackground()) { hashParts.background = this._parent.map.getBackground(); } const vu = this._parent.map.getVisibleUsers(); if(vu.length > 1 || !vu.includes("geovisio")) { hashParts.users = vu.join(","); } if(this._parent.map._mapFilters) { for(let k in MAP_FILTERS_JS2URL) { if(this._parent.map._mapFilters[k]) { hashParts[MAP_FILTERS_JS2URL[k]] = this._parent.map._mapFilters[k]; } } if(hashParts.pic_score) { const mapping = [null, "E", "D", "C", "B", "A"]; hashParts.pic_score = hashParts.pic_score.map(v => mapping[v]).join(""); } } } return hashParts; } /** * Compute next URL query string (based on `nextURLParams()`) * @memberof Panoramax.utils.URLHandler# * @return {string} The query string */ nextURLString() { let hash = ""; Object.entries(this.nextURLParams()) .sort((a,b) => a[0].localeCompare(b[0])) .forEach(entry => { let [ hashName, value ] = entry; let found = false; const parts = hash.split("&").map(part => { const key = part.split("=")[0]; if (key === hashName) { found = true; return `${key}=${value}`; } return part; }).filter(a => a); if (!found) { parts.push(`${hashName}=${value}`); } hash = `${parts.join("&")}`; }); return `?${hash}`.replace(/^\?+/, "?"); } /** * Transforms current URL query string into key->value object * @param {boolean} [readFromHash=false] Switch to reading from hash URL part (for retro-compatibility) * @return {object} Key-value read from current URL query * @memberof Panoramax.utils.URLHandler# */ currentURLParams(readFromHash = false) { // Get the current hash from location, stripped from its number sign const hash = (readFromHash ? window.location.hash : window.location.search).replace(/^[?#]/, ""); // Split the parameter-styled hash into parts and find the value we need let keyvals = {}; hash.split("&").map( part => part.split("=") ) .filter(part => part[0] !== undefined && part[0].length > 0 && MANAGED_PARAMETERS.includes(part[0])) .forEach(part => { keyvals[part[0]] = part[1]; }); // If hash is compressed if(keyvals.s) { const shortVals = Object.fromEntries( decodeURIComponent(keyvals.s) .split(";") .map(kv => [kv[0], kv.substring(1)]) ); keyvals = {}; // Used letters: b c d e f k m n p q s t u v // Focus if(shortVals.f === "m") { keyvals.focus = "map"; } else if(shortVals.f === "p") { keyvals.focus = "pic"; } else if(shortVals.f === "t") { keyvals.focus = "meta"; } // Speed if(shortVals.s !== "") { keyvals.speed = parseFloat(shortVals.s) * 100; } // Nav if(shortVals.n === "a") { keyvals.nav = "any"; } else if(shortVals.n === "s") { keyvals.nav = "seq"; } if(shortVals.n === "n") { keyvals.nav = "none"; } // Pic if(shortVals.p !== "") { keyvals.pic = shortVals.p; } // XYZ if(shortVals.c !== "") { keyvals.xyz = shortVals.c; } // Map if(shortVals.m !== "") { keyvals.map = shortVals.m; } // Date if(shortVals.d !== "") { keyvals.date_from = shortVals.d; } if(shortVals.e !== "") { keyvals.date_to = shortVals.e; } // Pic type if(shortVals.t === "f") { keyvals.pic_type = "flat"; } else if(shortVals.t === "e") { keyvals.pic_type = "equirectangular"; } // Camera if(shortVals.k !== "") { keyvals.camera = shortVals.k; } // Theme if(shortVals.v === "d") { keyvals.theme = "default"; } else if(shortVals.v === "a") { keyvals.theme = "age"; } else if(shortVals.v === "t") { keyvals.theme = "type"; } else if(shortVals.v === "s") { keyvals.theme = "score"; } // Background if(shortVals.b === "s") { keyvals.background = "streets"; } else if(shortVals.b === "a") { keyvals.background = "aerial"; } // Users if(shortVals.u !== "") { keyvals.users = shortVals.u; } // Photoscore if(shortVals.q !== "") { keyvals.pic_score = shortVals.q; } } return keyvals; } /** * Get string representation of map position * @returns {string} zoom/lat/lon or zoom/lat/lon/bearing/pitch * @memberof Panoramax.utils.URLHandler# */ currentMapString() { const center = this._parent.map.getCenter(), zoom = Math.round(this._parent.map.getZoom() * 100) / 100, // derived from equation: 512px * 2^z / 360 / 10^d < 0.5px precision = Math.ceil((zoom * Math.LN2 + Math.log(512 / 360 / 0.5)) / Math.LN10), m = Math.pow(10, precision), lng = Math.round(center.lng * m) / m, lat = Math.round(center.lat * m) / m, bearing = this._parent.map.getBearing(), pitch = this._parent.map.getPitch(); let hash = `${zoom}/${lat}/${lng}`; if (bearing || pitch) hash += (`/${Math.round(bearing * 10) / 10}`); if (pitch) hash += (`/${Math.round(pitch)}`); return hash; } /** * Get PSV view position as string * @returns {string} x/y/z * @memberof Panoramax.utils.URLHandler# */ currentPSVString() { const xyz = this._parent.psv.getXYZ(); const x = xyz.x.toFixed(2), y = xyz.y.toFixed(2), z = Math.round(xyz.z || 0); return `${x}/${y}/${z}`; } /** * Updates map and PSV according to current hash values * @private * @memberof Panoramax.utils.URLHandler# */ _onURLChange() { let vals = this.currentURLParams(); if(this._parent.getClassName() === "Viewer") { alterViewerState(this._parent, vals); } else { alterPhotoViewerState(this._parent, vals); } alterPSVState(this._parent.psv, vals); if(this._parent.map) { alterMapState(this._parent.map, vals); } } /** * Get short link URL (query replaced by Base64) * @returns {str} The short link URL * @memberof Panoramax.utils.URLHandler# */ nextShortLink(baseUrl) { const url = new URL(baseUrl); const hashParts = this.nextURLParams(); const shortVals = { f: (hashParts.focus || "").substring(0, 1), s: !isNaN(parseInt(hashParts.speed)) ? Math.floor(parseInt(hashParts.speed)/100) : undefined, n: (hashParts.nav || "").substring(0, 1), p: hashParts.pic, c: hashParts.xyz, m: hashParts.map, d: hashParts.date_from, e: hashParts.date_to, t: (hashParts.pic_type || "").substring(0, 1), k: hashParts.camera, v: (hashParts.theme || "").substring(0, 1), b: (hashParts.background || "").substring(0, 1), u: hashParts.users, q: hashParts.pic_score, }; const short = Object.entries(shortVals) .filter(([,v]) => v != undefined && v != "") .map(([k,v]) => `${k}${v}`) .join(";"); url.search = `s=${short}`; return url; } /** * Returns a string containing only parameters out of URLHandler scope * @param {URL} prevUrl The previously set URL * @memberof Panoramax.utils.URLHandler# */ getUnmanagedParameters(prevUrl) { return new URLSearchParams( Array.from(prevUrl.searchParams) .filter(([k]) => !MANAGED_PARAMETERS.includes(k)) ).toString(); } /** * Changes the URL hash using current viewer parameters * @private * @memberof Panoramax.utils.URLHandler# */ _onParentChange() { if(this._delay) { clearTimeout(this._delay); this._delay = null; } this._delay = setTimeout(() => { const prevUrl = new URL(window.location.href); const nextUrl = new URL(window.location.href); const unmanaged = this.getUnmanagedParameters(prevUrl); nextUrl.search = this._parent ? this.nextURLString() + (unmanaged.length > 0 ? "&"+unmanaged : ""): ""; // Clear out hash if older parameters appear if(Object.keys(this.currentURLParams(true)).length > 0) { nextUrl.hash = ""; } // Skip hash update if no changes if(prevUrl.search == nextUrl.search) { return; } const prevPic = this.currentURLParams().pic || ""; const nextPic = this._parent?.psv?.getPictureId?.() || ""; try { // If different pic, add entry in browser history if(prevPic != nextPic) { window.history.pushState(window.history.state, null, nextUrl.href); } // If same pic, just update viewer params else { window.history.replaceState(window.history.state, null, nextUrl.href); } if(this._parent) { /** * URL changed event * @event Panoramax.utils.URLHandler#url-changed * @type {CustomEvent} * @property {string} detail.url The new used URL */ const event = new CustomEvent("url-changed", { detail: {url: nextUrl.href}}); this.dispatchEvent(event); } } catch (SecurityError) { // IE11 does not allow this if the page is within an iframe created // with iframe.contentWindow.document.write(...). // https://github.com/mapbox/mapbox-gl-js/issues/7410 } }, 500); } }