UNPKG

terriajs

Version:

Geospatial data visualization platform.

391 lines 19.2 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import classNames from "classnames"; import { isEmpty, merge } from "lodash-es"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import Mustache from "mustache"; import { Component } from "react"; import { withTranslation } from "react-i18next"; import styled from "styled-components"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import TerriaError from "../../Core/TerriaError"; import filterOutUndefined from "../../Core/filterOutUndefined"; import isDefined from "../../Core/isDefined"; import { getName } from "../../ModelMixins/CatalogMemberMixin"; import DiscretelyTimeVaryingMixin from "../../ModelMixins/DiscretelyTimeVaryingMixin"; import MappableMixin from "../../ModelMixins/MappableMixin"; import TableMixin from "../../ModelMixins/TableMixin"; import TimeVarying from "../../ModelMixins/TimeVarying"; import FeatureInfoContext from "../../Models/Feature/FeatureInfoContext"; import Icon from "../../Styled/Icon"; import { withViewState } from "../Context"; import parseCustomMarkdownToReact from "../Custom/parseCustomMarkdownToReact"; import FeatureInfoDownload from "./FeatureInfoDownload"; import FeatureInfoPanelButton from "./FeatureInfoPanelButton"; import Styles from "./feature-info-section.scss"; import { generateCesiumInfoHTMLFromProperties } from "./generateCesiumInfoHTMLFromProperties"; import getFeatureProperties from "./getFeatureProperties"; import { mustacheFormatDateTime, mustacheFormatNumberFunction, mustacheRenderPartialByName, mustacheURLEncodeText, mustacheURLEncodeTextComponent } from "./mustacheExpressions"; // We use Mustache templates inside React views, where React does the escaping; don't escape twice, or eg. " => &quot; Mustache.escape = function (string) { return string; }; let FeatureInfoSection = class FeatureInfoSection extends Component { templateReactionDisposer; removeFeatureChangedSubscription; /** Rendered feature info template - this is set using reaction. * We can't use `@computed` values for custom templates - as CustomComponents may cause side-effects. * For example * - A CsvChartCustomComponent will create a new CsvCatalogItem and set traits * See `rawDataReactNode` for rendered raw data */ templatedFeatureInfoReactNode = undefined; noInfoRef = null; showRawData = false; observableFeature; observablePosition; observableCatalogItem; // Note this may not be known (eg. WFS). observableViewState; /** See `setFeatureChangedCounter` */ featureChangedCounter = 0; constructor(props) { super(props); makeObservable(this); this.observableFeature = props.feature; this.observablePosition = props.position; this.observableCatalogItem = props.catalogItem; this.observableViewState = props.viewState; } componentDidMount() { this.templateReactionDisposer = reaction(() => [ this.observableFeature, this.observableCatalogItem.featureInfoTemplate.template, this.observableCatalogItem.featureInfoTemplate.partials, // Note `mustacheContextData` will trigger update when `currentTime` changes (through this.featureProperties) this.mustacheContextData ], () => { if (this.observableCatalogItem.featureInfoTemplate.template && this.mustacheContextData) { this.templatedFeatureInfoReactNode = parseCustomMarkdownToReact(Mustache.render(this.observableCatalogItem.featureInfoTemplate.template, this.mustacheContextData, this.observableCatalogItem.featureInfoTemplate.partials), this.parseMarkdownContextData); } else { this.templatedFeatureInfoReactNode = undefined; } }, { fireImmediately: true }); this.setFeatureChangedCounter(this.observableFeature); } componentDidUpdate(prevProps) { runInAction(() => { this.observableFeature = this.props.feature; this.observablePosition = this.props.position; this.observableCatalogItem = this.props.catalogItem; this.observableViewState = this.props.viewState; }); if (prevProps.feature !== this.props.feature) { this.setFeatureChangedCounter(this.props.feature); } } /** Dispose of reaction and cesium feature change event listener */ componentWillUnmount() { this.templateReactionDisposer?.(); this.removeFeatureChangedSubscription?.(); } /** * We need to force `featureProperties` to re-compute when Cesium Feature properties change. * We use `featureChangedCounter` and increment it every change */ setFeatureChangedCounter(feature) { this.removeFeatureChangedSubscription?.(); this.removeFeatureChangedSubscription = feature.definitionChanged.addEventListener(((_changedFeature) => { runInAction(() => { this.featureChangedCounter++; }); }).bind(this)); } get currentTimeIfAvailable() { return TimeVarying.is(this.observableCatalogItem) ? this.observableCatalogItem.currentTimeAsJulianDate : undefined; } get featureProperties() { // Force computed to re-calculate when cesium feature properties change // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.featureChangedCounter; return getFeatureProperties(this.observableFeature, this.currentTimeIfAvailable ?? JulianDate.now(), MappableMixin.isMixedInto(this.observableCatalogItem) && this.observableCatalogItem.featureInfoTemplate.formats ? this.observableCatalogItem.featureInfoTemplate.formats : undefined); } /** This monstrosity contains properties which can be used by Mustache templates: * - All feature properties * - `properties` = array of key:value from feature properties * - `terria` magical object * - a bunch of custom mustache expressions * - `partialByName` * - `formatNumber` * - `formatDateTime` * - `urlEncodeComponent` * - `urlEncode` * - `coords` with `latitude` and `longitude` * - `currentTime` * - `rawDataTable` contains markdown table * - properties provided by catalog item through `featureInfoContext` function */ get mustacheContextData() { const propertyValues = Object.assign({}, this.featureProperties); // Properties accessible as {name, value} array; useful when you want // to iterate anonymous property values in the mustache template. const properties = Object.entries(propertyValues).map(([name, value]) => ({ name, value })); const propertyData = { ...propertyValues, properties, feature: this.observableFeature }; const terria = { partialByName: mustacheRenderPartialByName(this.observableCatalogItem.featureInfoTemplate?.partials ?? {}, propertyData), formatNumber: mustacheFormatNumberFunction, formatDateTime: mustacheFormatDateTime, urlEncodeComponent: mustacheURLEncodeTextComponent, urlEncode: mustacheURLEncodeText, rawDataTable: this.rawDataMarkdown }; if (this.observablePosition) { const latLngInRadians = Ellipsoid.WGS84.cartesianToCartographic(this.observablePosition); terria.coords = { latitude: CesiumMath.toDegrees(latLngInRadians.latitude), longitude: CesiumMath.toDegrees(latLngInRadians.longitude) }; } // Add currentTime property // if discrete - use current discrete time // otherwise - use current (continuous) time if (DiscretelyTimeVaryingMixin.isMixedInto(this.observableCatalogItem) && this.observableCatalogItem.currentDiscreteJulianDate) { terria.currentTime = JulianDate.toDate(this.observableCatalogItem.currentDiscreteJulianDate); } else if (TimeVarying.is(this.observableCatalogItem) && this.observableCatalogItem.currentTimeAsJulianDate) { terria.currentTime = JulianDate.toDate(this.observableCatalogItem.currentTimeAsJulianDate); } // Add activeStyle property if (TableMixin.isMixedInto(this.observableCatalogItem)) { terria.activeStyle = { id: this.observableCatalogItem.activeStyle }; } // If catalog item has featureInfoContext function // Merge it into other properties if (FeatureInfoContext.is(this.observableCatalogItem)) { return merge({ ...propertyData, terria }, this.observableCatalogItem.featureInfoContext(this.observableFeature) ?? {}); } return { ...propertyData, terria }; } clickHeader() { if (isDefined(this.props.onClickHeader)) { this.props.onClickHeader(this.observableFeature); } } /** Context object passed into "parseCustomMarkdownToReact" * These will get passed to CustomComponents (eg CsvChartCustomComponent) */ get parseMarkdownContextData() { return { terria: this.observableViewState.terria, catalogItem: this.observableCatalogItem, feature: this.observableFeature }; } /** Get raw data table as markdown string * * Will use feature.description if defined * Otherwise, will generate cesium info HTML table from feature properties */ get rawDataMarkdown() { const feature = this.observableFeature; const currentTime = this.currentTimeIfAvailable ?? JulianDate.now(); const description = feature.description?.getValue(currentTime); if (isDefined(description)) return description; if (isDefined(feature.properties)) { return generateCesiumInfoHTMLFromProperties(feature.properties, currentTime, MappableMixin.isMixedInto(this.observableCatalogItem) ? this.observableCatalogItem.showStringIfPropertyValueIsNull : undefined); } } /** Get Raw data as ReactNode. * Note: this can be computed - as no custom components are used which cause side-effects (eg CSVChartCustomComponent) * See `templatedFeatureInfoReactNode` for rendered feature info template */ get rawFeatureInfoReactNode() { if (this.rawDataMarkdown) return parseCustomMarkdownToReact(this.rawDataMarkdown, this.parseMarkdownContextData); } toggleRawData() { this.showRawData = !this.showRawData; } get downloadableData() { let fileName = getName(this.observableCatalogItem); // Add the Lat, Lon to the baseFilename if it is possible and not already present. if (this.observablePosition) { const position = Ellipsoid.WGS84.cartesianToCartographic(this.observablePosition); const latitude = CesiumMath.toDegrees(position.latitude); const longitude = CesiumMath.toDegrees(position.longitude); const precision = 5; // Check that baseFilename doesn't already contain the lat, lon with the similar or better precision. if (!contains(fileName, latitude, precision) || !contains(fileName, longitude, precision)) { fileName += " - Lat " + latitude.toFixed(precision) + " Lon " + longitude.toFixed(precision); } } return { data: this.featureProperties && !isEmpty(this.featureProperties) ? this.featureProperties : undefined, fileName }; } get generatedButtons() { const buttons = filterOutUndefined(this.observableViewState.featureInfoPanelButtonGenerators.map((generator) => { try { const dim = generator({ feature: this.observableFeature, item: this.observableCatalogItem }); return dim; } catch (error) { TerriaError.from(error).log(); } })); return buttons; } renderButtons() { const { t } = this.props; return (_jsxs(ButtonsContainer, { children: [!this.props.printView && this.templatedFeatureInfoReactNode && (_jsx(FeatureInfoPanelButton, { onClick: this.toggleRawData.bind(this), text: this.showRawData ? t("featureInfo.showCuratedData") : t("featureInfo.showRawData") })), this.generatedButtons.map((button, i) => (_jsx(FeatureInfoPanelButton, { ...button }, i)))] })); } render() { const { t } = this.props; let title; if (this.observableCatalogItem.featureInfoTemplate.name) { title = Mustache.render(this.observableCatalogItem.featureInfoTemplate.name, this.mustacheContextData, this.observableCatalogItem.featureInfoTemplate.partials); } else title = getName(this.observableCatalogItem) + " - " + (this.observableFeature.name || t("featureInfo.siteData")); /** Show feature info download if showing raw data - or showing template and `showFeatureInfoDownloadWithTemplate` is true */ const showFeatureInfoDownload = this.showRawData || !this.templatedFeatureInfoReactNode || (this.templatedFeatureInfoReactNode && this.observableCatalogItem.featureInfoTemplate .showFeatureInfoDownloadWithTemplate); const titleElement = this.props.printView ? (_jsx("h2", { children: title })) : (_jsxs("button", { type: "button", onClick: this.clickHeader.bind(this), className: Styles.title, children: [_jsx("span", { children: title }), this.props.isOpen ? (_jsx(Icon, { glyph: Icon.GLYPHS.opened })) : (_jsx(Icon, { glyph: Icon.GLYPHS.closed }))] })); // If feature is unavailable (or not showing) - show no info message if (!this.observableFeature.isAvailable(this.currentTimeIfAvailable ?? JulianDate.now()) || !this.observableFeature.isShowing) { return (_jsxs("li", { className: classNames(Styles.section), children: [titleElement, this.props.isOpen ? (_jsx("section", { className: Styles.content, children: _jsx("div", { ref: (r) => { this.noInfoRef = r; }, children: t("featureInfo.noInfoAvailable") }, "no-info") })) : null] })); } return (_jsxs("li", { className: classNames(Styles.section), children: [titleElement, this.props.isOpen ? (_jsxs("section", { className: Styles.content, children: [this.renderButtons(), _jsxs("div", { children: [this.observableFeature.loadingFeatureInfoUrl ? ("Loading") : this.showRawData || !this.templatedFeatureInfoReactNode ? (this.rawFeatureInfoReactNode ? (this.rawFeatureInfoReactNode) : (_jsx("div", { ref: (r) => { this.noInfoRef = r; }, children: t("featureInfo.noInfoAvailable") }, "no-info"))) : ( // Show templated feature info this.templatedFeatureInfoReactNode), // Show FeatureInfoDownload !this.props.printView && showFeatureInfoDownload && isDefined(this.downloadableData.data) ? (_jsx(FeatureInfoDownload, { data: this.downloadableData.data, name: this.downloadableData.fileName }, "download")) : null] })] })) : null] })); } }; __decorate([ observable.ref ], FeatureInfoSection.prototype, "templatedFeatureInfoReactNode", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "showRawData", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "observableFeature", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "observablePosition", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "observableCatalogItem", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "observableViewState", void 0); __decorate([ observable ], FeatureInfoSection.prototype, "featureChangedCounter", void 0); __decorate([ action ], FeatureInfoSection.prototype, "setFeatureChangedCounter", null); __decorate([ computed ], FeatureInfoSection.prototype, "currentTimeIfAvailable", null); __decorate([ computed ], FeatureInfoSection.prototype, "featureProperties", null); __decorate([ computed ], FeatureInfoSection.prototype, "mustacheContextData", null); __decorate([ computed ], FeatureInfoSection.prototype, "parseMarkdownContextData", null); __decorate([ computed ], FeatureInfoSection.prototype, "rawDataMarkdown", null); __decorate([ computed ], FeatureInfoSection.prototype, "rawFeatureInfoReactNode", null); __decorate([ action ], FeatureInfoSection.prototype, "toggleRawData", null); __decorate([ computed ], FeatureInfoSection.prototype, "downloadableData", null); __decorate([ computed ], FeatureInfoSection.prototype, "generatedButtons", null); FeatureInfoSection = __decorate([ observer ], FeatureInfoSection); export { FeatureInfoSection }; // See if text contains the number (to a precision number of digits (after the dp) either fixed up or down on the last digit). function contains(text, number, precision) { // Take Math.ceil or Math.floor and use it to calculate the number with a precision number of digits (after the dp). function fixed(round, number) { const scale = Math.pow(10, precision); return (round(number * scale) / scale).toFixed(precision); } return (text.indexOf(fixed(Math.floor, number)) !== -1 || text.indexOf(fixed(Math.ceil, number)) !== -1); } const ButtonsContainer = styled.div ` display: flex; justify-content: flex-end; padding: 7px 0 10px 0; `; export default withTranslation()(withViewState(FeatureInfoSection)); //# sourceMappingURL=FeatureInfoSection.js.map