UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

382 lines (353 loc) 13.1 kB
import { LitElement, nothing, css } from "lit"; import { html, unsafeStatic } from "lit/static-html.js"; import { fa, onceParentAvailable } from "../../utils/widgets"; import { faLocationDot } from "@fortawesome/free-solid-svg-icons/faLocationDot"; import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal"; import { faCamera } from "@fortawesome/free-solid-svg-icons/faCamera"; import { faImage } from "@fortawesome/free-solid-svg-icons/faImage"; import { faImages } from "@fortawesome/free-solid-svg-icons/faImages"; import { faScroll } from "@fortawesome/free-solid-svg-icons/faScroll"; import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import { faTags } from "@fortawesome/free-solid-svg-icons/faTags"; import { faSvg, titles, textarea, hidden, dataBlocks } from "../styles"; import { createWebComp } from "../../utils/widgets"; import { getGPSPrecision } from "../../utils/picture"; import { PanoramaxMetaCatalogURL } from "../../utils/services"; import { getGrade, QUALITYSCORE_GPS_VALUES, QUALITYSCORE_RES_360_VALUES, QUALITYSCORE_RES_FLAT_VALUES, QUALITYSCORE_POND_GPS, QUALITYSCORE_POND_RES } from "../../utils/utils"; const missing = () => fa(faQuestion, {styles: {height: "16px"}}); /** * Picture metadata displays detailed info about a single picture (ID, capture context, EXIF attributes...). * @class Panoramax.components.menus.PictureMetadata * @element pnx-picture-metadata * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/) * @example * ```html * <pnx-picture-metadata ._parent=${viewer} /> * ``` */ export default class PictureMetadata extends LitElement { /** @private */ static styles = [ faSvg, titles, textarea, hidden, dataBlocks, css` div[slot="content"] { padding: 5px 10px; background-color: #ededed; } .data-block { min-width: 50%; } pnx-semantics-table { overflow-x: auto; display: block; } ` ]; /** * Component properties. * @memberof Panoramax.components.menus.PictureMetadata# * @type {Object} * @property {boolean} [block-editor=false] Set to true to avoid going beyond semantics editor */ static properties = { _meta: {state: true}, "block-editor": { type: Boolean }, }; /** @private */ connectedCallback() { super.connectedCallback(); const gototags = () => { const tabs = this.shadowRoot.querySelector("pnx-tabs"); if(tabs) { tabs.setAttribute("activeTabIndex", 4); } }; onceParentAvailable(this).then(() => { this._meta = this._parent?.psv?.getPictureMetadata(); this._parent?.oncePSVReady?.().then(() => { this._parent.psv.addEventListener("picture-loaded", () => { this._meta = this._parent.psv.getPictureMetadata(); }); this._parent.psv.addEventListener("annotation-focused", gototags); this._parent.psv.addEventListener("annotations-toggled", e => { if(e.detail.visible) { gototags(); } }); if(this._parent.psv.getSelectedAnnotations().length > 0) { gototags(); } }); }); } /** @private */ _onQualityScoreClick() { const qsTab = this.shadowRoot.querySelector("pnx-tabs"); if(qsTab) { qsTab.setAttribute("activeTabIndex", 3); } } /** @private */ _onCaptureDateClick() { const qsTab = this.shadowRoot.querySelector("pnx-tabs"); if(qsTab) { qsTab.setAttribute("activeTabIndex", 1); } } /** @private */ _toTab(title, data) { return html` <h4 slot="title">${title}</h4> <div slot="content" class="data-blocks"> ${data.filter(b => b).map(b => html`<div class="data-block" style=${b.style}> <h5>${b.title}</h5> <div style=${b.content_style} @click=${b.click}>${b.content}</div> </div>`)} </div> `; } /** @private */ render() { /* eslint-disable indent */ if(!this._meta) { return nothing; } const lang = this._parent?.lang || window.navigator.language; // Generic information const persOrient = this._meta?.properties?.["pers:interior_orientation"]; const makeModel = [persOrient.camera_manufacturer, persOrient.camera_model].filter(v => v).join(" "); const focal = persOrient?.focal_length ? `${persOrient?.focal_length} mm` : missing(); let resmp = persOrient?.["sensor_array_dimensions"]; if(resmp) { resmp = `${resmp[0]} x ${resmp[1]} px (${Math.floor(resmp[0] * resmp[1] / 1000000)} Mpx)`;} let pictype = this._parent?._t.pnx.picture_flat; let pictypelong = this._parent?._t.pnx.picture_flat_long; let picFov = persOrient?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up if(picFov !== null && picFov !== undefined) { if(picFov === 360) { pictype = this._parent?._t.pnx.picture_360; pictypelong = this._parent?._t.pnx.picture_360_long; } else { pictype += ` (${picFov}°)`; } } // Camera tab const cameraData = [ { title: this._parent?._t.pnx.metadata_camera_make, content: persOrient?.camera_manufacturer || missing() }, { title: this._parent?._t.pnx.metadata_camera_model, content: persOrient?.camera_model || missing() }, { title: this._parent?._t.pnx.metadata_camera_type, content: pictype }, { title: this._parent?._t.pnx.metadata_camera_resolution, content: resmp || missing() }, { title: this._parent?._t.pnx.metadata_camera_focal_length, content: focal }, // Capture date this._meta?.caption?.date && { title: this._parent?._t.pnx.metadata_general_date, content: html` <strong>${new Intl.DateTimeFormat(lang, { timeZone: this._meta.caption.tz, dateStyle: "short" }).format(this._meta.caption.date)}</strong> <br />${new Intl.DateTimeFormat(lang, { timeZone: this._meta.caption.tz, hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3, timeZoneName: "longOffset" }).format(this._meta.caption.date)} ` } ]; // Location tab const orientation = this._meta?.properties?.["view:azimuth"] !== undefined && `${this._meta.properties["view:azimuth"]}°`; const locationData = [ { title: this._parent?._t.pnx.metadata_location_coordinates, content: html` ${this._meta.gps[0]}, ${this._meta.gps[1]} <pnx-copy-coordinates ._parent=${this._parent} .gps=${this._meta.gps} style="margin-left: 10px" ></pnx-copy-coordinates> `, style: "width: 100%" }, { title: this._parent?._t.pnx.metadata_location_orientation, content: orientation || missing() }, { title: this._parent?._t.pnx.metadata_location_precision, content: getGPSPrecision(this._meta) || missing() }, ]; // Quality tab const hasQualityScore = ( this._parent?.map?._hasQualityScore?.() || this._meta?.properties?.["quality:horizontal_accuracy"] || this._meta?.properties?.["panoramax:horizontal_pixel_density"] ); let qualityData, generalGrade; if(hasQualityScore) { const gpsGrade = getGrade(QUALITYSCORE_GPS_VALUES, this._meta?.properties?.["quality:horizontal_accuracy"]); const resGrade = getGrade( this._meta?.horizontalFov === 360 ? QUALITYSCORE_RES_360_VALUES : QUALITYSCORE_RES_FLAT_VALUES, this._meta?.properties?.["panoramax:horizontal_pixel_density"] ); // Note: score is also calculated in utils/map code generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS); qualityData = [ { title: html` ${this._parent?._t.pnx.metadata_quality_score} <pnx-button title="${this._parent?._t.pnx.metadata_quality_help}" kind="superinline" @click=${() => this._parent?._showQualityScoreDoc()} > ${fa(faInfoCircle)} </pnx-button> `, content: html`<pnx-quality-score grade=${generalGrade} style="font-size: 16px"></pnx-quality-score>`, style: "width: 100%" }, { title: this._parent?._t.pnx.metadata_quality_gps_score, content: createWebComp("pnx-grade", { stars: gpsGrade, _t: this._parent?._t }) }, { title: this._parent?._t.pnx.metadata_quality_resolution_score, content: createWebComp("pnx-grade", { stars: resGrade, _t: this._parent?._t }) }, ]; } // EXIF data const exifData = Object.entries(this._meta.properties.exif) .sort() .filter(([, value]) => value) .map(([key, value]) => { if(JSON.stringify(value).includes("\\u")) { value = JSON.stringify(value).replace(/\\u[0-9A-Fa-f]{4}/g, unicode => ( " 0x" + parseInt(unicode.slice(2), 16).toString(16).toUpperCase().padStart(4, "0") )).slice(1, -1).trim(); } return { title: key, content: value.length > 30 ? html`<textarea readonly .value=${value}></textarea>`: value, style: value.length > 30 ? "width: 100%" : undefined }; }); exifData.unshift({ content: html` <pnx-link-button size="sm" target="_blank" href="https://exiv2.org/metadata.html" title=${this._parent?._t.pnx.metadata_exif_doc_title} >${fa(faInfoCircle)} ${this._parent?._t.pnx.metadata_exif_doc}</pnx-link-button> `, style: "width: 100%" }); // General metadata const overview = [ // Producer this._meta?.caption?.producer?.length > 0 && { title: this._parent?._t.pnx.metadata_general_author, content: html` <strong>${this._meta.caption.producer[this._meta.caption.producer.length - 1]}</strong> ${this._meta.caption.producer.length > 1 ? html`<br />${this._meta.caption.producer.slice(0, -1).join(", ")}` : nothing} ` }, // Capture date this._meta?.caption?.date && { title: this._parent?._t.pnx.metadata_general_date, click: this._onCaptureDateClick, content_style: "cursor: pointer", content: html` <strong>${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, dateStyle: "long"}).format(this._meta.caption.date)}</strong> <br />${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, hour: "numeric",minute:"numeric", second: "numeric"}).format(this._meta.caption.date)} ` }, // Camera persOrient && { title: this._parent?._t.pnx.metadata_camera, content: html` <strong>${makeModel.length > 0 ? makeModel : missing()}</strong> <br />${pictypelong} ` }, // License this._meta?.caption?.license && { title: this._parent?._t.pnx.metadata_general_license, content: html`${unsafeStatic(this._meta.caption.license)}` }, // Quality score hasQualityScore && { title: this._parent?._t.pnx.metadata_quality, click: this._onQualityScoreClick, content: html`<pnx-quality-score grade=${generalGrade} style="font-size: 14px; cursor: pointer" />` }, // Copy ID { title: this._parent?._t.pnx.metadata_general_copy_id, content_style: "display: flex; gap: 5px;", content: html` <pnx-copy-button kind="outline" size="sm" ._t=${this._parent?._t} text=${this._meta.id} title=${this._meta.id} style="flex: 1" > ${fa(faImage)} ${this._parent?._t.pnx.metadata_general_picid} </pnx-copy-button> <pnx-copy-button kind="outline" size="sm" ._t=${this._parent?._t} text=${this._meta.sequence.id} title=${this._meta.sequence.id} style="flex: 1" > ${fa(faImages)} ${this._parent?._t.pnx.metadata_general_seqid} </pnx-copy-button> ` }, this._meta?.origInstance && { title: this._parent?._t.pnx.metadata_general_instance, content: html`<strong> <a href=${this._meta.origInstance.href+window.location.search} target="_blank" style="text-decoration: none"> <img src=${PanoramaxMetaCatalogURL()+"/api/instances/"+this._meta.origInstance.instance_name+"/logo"} style="height: 30px; max-width: 150px; vertical-align: middle" alt="" /> <span style="text-decoration: underline"> ${this._meta.origInstance.instance_name || this._meta.origInstance.href.replace(/^http.?:\/\//, "")} </span> </a> </strong>` } ]; return html`<pnx-tabs .hideNav=${this["block-editor"]}> ${this._toTab( // General html`${fa(faImage)} ${this._parent?._t.pnx.metadata_summary}`, overview )} ${this._toTab( // Camera html`${fa(faCamera)} ${this._parent?._t.pnx.metadata_camera}`, cameraData )} ${this._toTab( // Position html`${fa(faLocationDot)} ${this._parent?._t.pnx.metadata_location}`, locationData )} ${hasQualityScore ? this._toTab( // Quality html`${fa(faMedal)} ${this._parent?._t.pnx.metadata_quality}`, qualityData ) : nothing} <h4 slot="title">${fa(faTags)} ${this._parent?._t.pnx.semantics_title}</h4> <div slot="content" class="data-blocks"> <pnx-semantics-metadata ._parent=${this._parent}></pnx-semantics-metadata> </div> ${this._meta.properties?.exif ? this._toTab( // EXIF html`${fa(faScroll)} ${this._parent?._t.pnx.metadata_exif}`, exifData ) : nothing} </pnx-tabs>`; } } customElements.define("pnx-picture-metadata", PictureMetadata);