UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

365 lines (329 loc) • 11.5 kB
import { LitElement, html } from "lit"; import API from "../../utils/API"; import { getTranslations } from "../../utils/i18n"; import { MapTiles } from "../../utils/services"; import { createWebComp } from "../../utils/widgets"; import { isInIframe, isInternetFast } from "../../utils/utils"; import JSON5 from "json5"; import PACKAGE_JSON from "../../../package.json"; import "@fontsource/atkinson-hyperlegible-next"; import "./Basic.css"; /** * Event for overlaying menu opening * @event Panoramax.components.core.Basic#menu-opened * @type {CustomEvent} * @property {Element} detail.menu The opened menu */ /** * Basic core component is a basic container for common functions through all core components. * It is not intended to be used directly, it's only to be extended by other core components. * @class Panoramax.components.core.Basic * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/) * @fires Panoramax.components.core.Basic#select * @fires Panoramax.components.core.Basic#ready * @fires Panoramax.components.core.Basic#broken * @fires Panoramax.components.core.Basic#menu-opened * @property {Panoramax.components.ui.Loader} loader The loader screen * @property {Panoramax.utils.API} api The API manager */ export default class Basic extends LitElement { /** * Component properties. * @memberof Panoramax.components.core.Basic# * @type {Object} * @mixin * @property {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md)) * @property {string} [picture] The picture ID to display * @property {string} [sequence] The sequence ID of the picture displayed * @property {object} [fetch-options] Set custom options for fetch calls made against API ([same syntax as fetch options parameter](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters)) * @property {string[]} [users=[geovisio]] List of users IDs to use for map display (defaults to general map, identified as "geovisio") * @property {string|object} [map-style] The map's MapLibre style. This can be an a JSON object conforming to the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/), or a URL string pointing to one. Defaults to OSM vector tiles. * @property {string} [lang] To override language used for labels. Defaults to using user's preferred languages. */ static properties = { picture: {type: String, reflect: true}, sequence: {type: String, reflect: true}, "fetch-options": {converter: Basic.GetJSONConverter()}, users: {type: Array, reflect: true}, "map-style": {type: String}, lang: {type: String}, endpoint: {type: String}, }; constructor(testing = false) { super(); // Some defaults this.users = ["geovisio"]; this["map-style"] = this.getAttribute("map-style") || MapTiles(); this.lang = this.getAttribute("lang") || null; this.endpoint = this.getAttribute("endpoint") || null; // No default this.picture = this.getAttribute("picture") || null; this.sequence = this.getAttribute("sequence") || null; // Display version in logs console.info(`šŸ“· Panoramax ${this.getClassName()} - Version ${PACKAGE_JSON.version} (${__COMMIT_HASH__}) šŸ†˜ Issues can be reported at ${PACKAGE_JSON.repository.url}`); if(testing) { return; } // Internet speed check this._isInternetFast = null; isInternetFast().then(isFast => this._isInternetFast = isFast); } connectedCallback() { super.connectedCallback(); // Translations this._t = getTranslations(this.lang); // Show loader this.loader = createWebComp("pnx-loader", {_parent: this, "no-label": isInIframe() }); if( !(this._loadsAPI && this.endpoint && this._loadsAPI === this.endpoint) && !(this.api && this.api._endpoint === this.endpoint) && this.endpoint ) { if(this._loadsAPI || this.api) { delete this.api; delete this._loadsAPI; } this._setupAPI(); } // Warn for outdate attributes Object .entries({ map: "map-options", psv: "psv-options", fetchOptions: "fetch-options", mapstyle: "map-style"}) .forEach(([k, v]) => { if(this.getAttribute(k)) { console.error(`Component attribute "${k}" has been renamed into "${v}". Old attribute "${k}" is ignored.`); } }); } /** * Creates API and wait for initial loading * @private */ _setupAPI() { // Loader init this.loader = this.loader || createWebComp("pnx-loader", {_parent: this}); if(!this.endpoint) { console.warn("No endpoint is defined"); return; } this._loadsAPI = this.endpoint; let myLoadAPI = this.endpoint; // Check if mapstyle is not a unparsed JSON try { this["map-style"] = JSON.parse(this["map-style"]); } catch(e) { /* empty */ } // API init try { this.api = new API(this.endpoint, { users: this.users, fetch: this["fetch-options"], style: this["map-style"], }); this.api.onceReady() .then(() => { if(myLoadAPI != this._loadsAPI || !this.api) { return; } let unavailable = this.api.getUnavailableFeatures(); let available = this.api.getAvailableFeatures(); available = unavailable.length === 0 ? "āœ… All features available" : "āœ… Available features: "+available.join(", "); unavailable = unavailable.length === 0 ? "" : "🚫 Unavailable features: "+unavailable.join(", "); console.info(`🌐 Connected to API "${this.api._metadata.name}" (${this.api._endpoint}) ā„¹ļø API runs STAC ${this.api._metadata.stac_version} ${this.api._metadata.geovisio_version ? "& GeoVisio "+this.api._metadata.geovisio_version : ""} ${available} ${unavailable} `.trim()); }) .catch(e => this.loader.dismiss(e, this._t.pnx.error_api)) .finally(() => delete this._loadsAPI); } catch(e) { delete this._loadsAPI; if(this.loader?.dismiss) { this.loader.dismiss(e, this._t.pnx.error_api); } else { console.error(e); } } } /** * Waits for component to have its first loading done. * * Each inheriting class must override this method. * @memberof Panoramax.components.core.Basic# * @returns {Promise} * @fulfil {null} When initialization is complete. * @reject {string} Error message */ onceReady() { throw new Error("You must override this method on sub-class"); } /** * Waits for initial API setup. * @memberof Panoramax.components.core.Basic# * @returns {Promise} * @fulfil {null} When API is ready. * @reject {string} Error message */ onceAPIReady() { if(this.api) { return this.api.onceReady(); } else { return new Promise(resolve => setTimeout(resolve, 100)).then(this.onceAPIReady.bind(this)); } } /** @private */ createRenderRoot() { return this; } /** @private */ attributeChangedCallback(name, _old, value) { super.attributeChangedCallback(name, _old, value); if(name === "endpoint") { if( !(this._loadsAPI && value && this._loadsAPI === value) && !(this.api && this.api._endpoint === value) && value ) { if(this._loadsAPI || this.api) { delete this.api; delete this._loadsAPI; } this._setupAPI(); } } if(["picture", "sequence"].includes(name)) { let seqId, picId, prevSeqId, prevPicId; if(name === "picture") { seqId = this.sequence; prevSeqId = this.sequence; picId = value; prevPicId = _old; } else { seqId = value; prevSeqId = _old; picId = this.picture; prevPicId = this.picture; } /** * Event for sequence/picture selection * @event Panoramax.components.core.Basic#select * @type {CustomEvent} * @property {string} detail.seqId The selected sequence ID * @property {string} detail.picId The selected picture ID (or null if not a precise picture clicked) * @property {string} [detail.prevSeqId] The previously selected sequence ID (or null if none) * @property {string} [detail.prevPicId] The previously selected picture ID (or null if none) */ this.dispatchEvent(new CustomEvent("select", { bubbles: true, composed: true, detail: { seqId, picId, prevSeqId, prevPicId, } })); } } /** * This allows to retrieve an always correct class name. * This is crap, but avoids issues with Webpack & so on. * * Each inheriting class must override this method. * @returns {string} The class name (for example "Basic") * @memberof Panoramax.components.core.Basic# */ getClassName() { return "Basic"; } /** * Change the currently picture and/or sequence. * Calling the method without parameters unselects. * @param {string} [seqId] The sequence UUID * @param {string} [picId] The picture UUID * @param {boolean} [force=false] Force select even if already selected * @memberof Panoramax.components.core.Basic# */ select(seqId = null, picId = null, force = false) { if(force) { this.picture = null; this.sequence = null; } this.picture = picId; this.sequence = seqId; } /** * Is the view running in a small container (small embed or smartphone) * @returns {boolean} True if container is small * @memberof Panoramax.components.core.Basic# */ isWidthSmall() { return this?.offsetWidth < 576; } /** * Is the view running in a small-height container (small embed or smartphone) * @returns {boolean} True if container height is small * @memberof Panoramax.components.core.Basic# */ isHeightSmall() { return this?.offsetHeight < 400; } /** @private */ render() { return html`<p>Should not be used directly, use Viewer/CoverageMap/Editor instead</p>`; } /** * List names of sub-components (like loader, api, map, psv) available in this component. * @returns {string[]} Sub-components names. * @memberof Panoramax.components.core.Basic# */ getSubComponentsNames() { return ["loader", "api"]; } /** * Listen to events from this components or one of its sub-components. * * For example, you can listen to `map` events using prefix `map:`. * * ```js * me.addEventListener("map:move", doSomething); * ``` * @param {string} type The event type to listen for * @param {function} listener The event handler * @param {object} [options] [Any original addEventListener available options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) * @memberof Panoramax.components.core.Basic# */ addEventListener(type, listener, options) { // Check if listener is for sub-component let prefix = type.split(":").shift(); if(prefix && this.getSubComponentsNames().includes(prefix)) { const subType = type.substring(prefix.length+1); // Add directly if available if(this[prefix]?.addEventListener) { this[prefix].addEventListener(subType, listener, options); } // Wait for addEventListener to be available else { setTimeout(() => this.addEventListener(type, listener, options), 50); } } // Otherwise, reuse classic function else { super.addEventListener(type, listener, options); } } /** @private */ static GetJSONConverter() { return { fromAttribute: (value) => { if(value === null || value === "") { return null; } else if(typeof value === "object" || Array.isArray(value)) { return value; } else { return JSON5.parse(value); } }, toAttribute: (value) => { if(value === null || value === "") { return ""; } else if(typeof value === "string") { return value; } else { return JSON5.stringify(value); } } }; } }