terriajs
Version:
Geospatial data visualization platform.
290 lines • 15.6 kB
JavaScript
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 { action, reaction, runInAction, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { Component } from "react";
import { withTranslation } from "react-i18next";
import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import flatten from "../../Core/flatten";
import isDefined from "../../Core/isDefined";
import { featureBelongsToCatalogItem } from "../../Map/PickedFeatures/PickedFeatures";
import prettifyCoordinates from "../../Map/Vector/prettifyCoordinates";
import MappableMixin from "../../ModelMixins/MappableMixin";
import TimeFilterMixin from "../../ModelMixins/TimeFilterMixin";
import CompositeCatalogItem from "../../Models/Catalog/CatalogItems/CompositeCatalogItem";
import TerriaFeature from "../../Models/Feature/Feature";
import { addMarker, isMarkerVisible, removeMarker } from "../../Models/LocationMarkerUtils";
import Icon from "../../Styled/Icon";
import Loader from "../Loader";
import { withViewState } from "../Context";
import Styles from "./feature-info-panel.scss";
import FeatureInfoCatalogItem from "./FeatureInfoCatalogItem";
import DragWrapper from "../Drag/DragWrapper";
let FeatureInfoPanel = class FeatureInfoPanel extends Component {
pickedFeaturesReactionDisposer = undefined;
constructor(props) {
super(props);
makeObservable(this);
}
componentDidMount() {
const { t } = this.props;
const terria = this.props.viewState.terria;
this.pickedFeaturesReactionDisposer = reaction(() => terria.pickedFeatures, (pickedFeatures) => {
if (!isDefined(pickedFeatures)) {
terria.selectedFeature = undefined;
}
else {
terria.selectedFeature = TerriaFeature.fromEntity(new Entity({
id: t("featureInfo.pickLocation"),
position: pickedFeatures.pickPosition
}));
if (isDefined(pickedFeatures.allFeaturesAvailablePromise)) {
pickedFeatures.allFeaturesAvailablePromise.then(() => {
if (this.props.viewState.featureInfoPanelIsVisible === false) {
// Panel is closed, refrain from setting selectedFeature
return;
}
// We only show features that are associated with a catalog item, so make sure the one we select to be
// open initially is one we're actually going to show.
const featuresShownAtAll = pickedFeatures.features.filter((x) => isDefined(determineCatalogItem(terria.workbench, x)));
// Return if `terria.selectedFeatures` already showing a valid feature?
if (featuresShownAtAll.some((feature) => feature === terria.selectedFeature))
return;
// Otherwise find first feature with data to show
let selectedFeature = featuresShownAtAll.filter((feature) => isDefined(feature.properties) ||
isDefined(feature.description))[0];
if (!isDefined(selectedFeature) &&
featuresShownAtAll.length > 0) {
// Handles the case when no features have info - still want something to be open.
selectedFeature = featuresShownAtAll[0];
}
runInAction(() => {
terria.selectedFeature = selectedFeature;
});
});
}
}
});
}
componentWillUnmount() {
if (isDefined(this.pickedFeaturesReactionDisposer)) {
this.pickedFeaturesReactionDisposer();
}
}
renderFeatureInfoCatalogItems(catalogItems, featureMap) {
return catalogItems.map((catalogItem, _i) => {
// From the pairs, select only those with this catalog item, and pull the features out of the pair objects.
const features = (catalogItem.uniqueId
? featureMap.get(catalogItem.uniqueId)
: undefined) ?? [];
return (_jsx(FeatureInfoCatalogItem, { viewState: this.props.viewState, catalogItem: catalogItem, features: features, onToggleOpen: this.toggleOpenFeature, printView: this.props.printView }, catalogItem.uniqueId));
});
}
close() {
this.props.viewState.featureInfoPanelIsVisible = false;
// give the close animation time to finish before unselecting, to avoid jumpiness
setTimeout(action(() => {
this.props.viewState.terria.pickedFeatures = undefined;
this.props.viewState.terria.selectedFeature = undefined;
}), 200);
}
toggleCollapsed() {
this.props.viewState.featureInfoPanelIsCollapsed =
!this.props.viewState.featureInfoPanelIsCollapsed;
}
toggleOpenFeature(feature) {
const terria = this.props.viewState.terria;
if (feature === terria.selectedFeature) {
terria.selectedFeature = undefined;
}
else {
terria.selectedFeature = feature;
}
}
getMessageForNoResults() {
const { t } = this.props;
if (this.props.viewState.terria.workbench.items.length > 0) {
// feature info shows up becuase data has been added for the first time
if (this.props.viewState.firstTimeAddingData) {
runInAction(() => {
this.props.viewState.firstTimeAddingData = false;
});
return t("featureInfo.clickMap");
}
// if clicking on somewhere that has no data
return t("featureInfo.noDataAvailable");
}
else {
return t("featureInfo.clickToAddData");
}
}
addManualMarker(longitude, latitude) {
const { t } = this.props;
addMarker(this.props.viewState.terria, {
name: t("featureInfo.userSelection"),
location: {
latitude: latitude,
longitude: longitude
}
});
}
pinClicked(longitude, latitude) {
if (!isMarkerVisible(this.props.viewState.terria)) {
this.addManualMarker(longitude, latitude);
}
else {
removeMarker(this.props.viewState.terria);
}
}
// locationUpdated(longitude, latitude) {
// if (
// isDefined(latitude) &&
// isDefined(longitude) &&
// isMarkerVisible(this.props.viewState.terria)
// ) {
// removeMarker(this.props.viewState.terria);
// this.addManualMarker(longitude, latitude);
// }
// }
filterIntervalsByFeature(catalogItem, feature) {
try {
catalogItem.setTimeFilterFeature(feature, this.props.viewState.terria.pickedFeatures?.providerCoords);
}
catch (e) {
this.props.viewState.terria.raiseErrorToUser(e);
}
}
renderLocationItem(cartesianPosition) {
const cartographic = Ellipsoid.WGS84.cartesianToCartographic(cartesianPosition);
if (cartographic === undefined) {
return null;
}
const latitude = CesiumMath.toDegrees(cartographic.latitude);
const longitude = CesiumMath.toDegrees(cartographic.longitude);
const pretty = prettifyCoordinates(longitude, latitude);
// this.locationUpdated(longitude, latitude);
const that = this;
const pinClicked = function () {
that.pinClicked(longitude, latitude);
};
const locationButtonStyle = isMarkerVisible(this.props.viewState.terria)
? Styles.btnLocationSelected
: Styles.btnLocation;
return (_jsxs("div", { className: Styles.location, children: [_jsx("span", { children: "Lat / Lon\u00A0" }), _jsxs("span", { children: [pretty.latitude + ", " + pretty.longitude, !this.props.printView && (_jsx("button", { type: "button", onClick: pinClicked, className: locationButtonStyle, children: _jsx(Icon, { glyph: Icon.GLYPHS.location }) }))] })] }));
}
render() {
const { t } = this.props;
const terria = this.props.viewState.terria;
const viewState = this.props.viewState;
const { catalogItems, featureMap } = getFeatureMapByCatalogItems(this.props.viewState.terria);
const featureInfoCatalogItems = this.renderFeatureInfoCatalogItems(catalogItems, featureMap);
const panelClassName = classNames(Styles.panel, {
[Styles.isCollapsed]: viewState.featureInfoPanelIsCollapsed,
[Styles.isVisible]: viewState.featureInfoPanelIsVisible,
[Styles.isTranslucent]: viewState.explorerPanelIsVisible
});
const filterableCatalogItems = catalogItems
.filter((catalogItem) => TimeFilterMixin.isMixedInto(catalogItem) &&
catalogItem.canFilterTimeByFeature)
.map((catalogItem) => {
const features = (catalogItem.uniqueId
? featureMap.get(catalogItem.uniqueId)
: undefined) ?? [];
return {
catalogItem: catalogItem,
feature: isDefined(features[0]) ? features[0] : undefined
};
})
.filter((pair) => isDefined(pair.feature));
// If the clock is available then use it, otherwise don't.
const clock = terria.timelineClock?.currentTime;
// If there is a selected feature then use the feature location.
let position = terria.selectedFeature?.position?.getValue(clock);
// If position is invalid then don't use it.
// This seems to be fixing the symptom rather then the cause, but don't know what is the true cause this ATM.
if (position === undefined ||
isNaN(position.x) ||
isNaN(position.y) ||
isNaN(position.z)) {
position = undefined;
}
if (!isDefined(position)) {
// Otherwise use the location picked.
position = terria.pickedFeatures?.pickPosition;
}
const locationElements = position ? (_jsx("li", { children: this.renderLocationItem(position) })) : null;
return (_jsx(DragWrapper, { handleSelector: ".drag-handle", children: _jsxs("div", { className: panelClassName, "aria-hidden": !viewState.featureInfoPanelIsVisible, children: [!this.props.printView && (_jsxs("div", { className: Styles.header, children: [_jsxs("div", { className: classNames("drag-handle", Styles.btnPanelHeading), children: [_jsx("span", { children: t("featureInfo.panelHeading") }), _jsx("button", { type: "button", onClick: this.toggleCollapsed, className: Styles.btnToggleFeature, children: this.props.viewState.featureInfoPanelIsCollapsed ? (_jsx(Icon, { glyph: Icon.GLYPHS.closed })) : (_jsx(Icon, { glyph: Icon.GLYPHS.opened })) })] }), _jsx("button", { type: "button", onClick: this.close, className: Styles.btnCloseFeature, title: t("featureInfo.btnCloseFeature"), children: _jsx(Icon, { glyph: Icon.GLYPHS.close }) })] })), _jsxs("ul", { className: Styles.body, children: [this.props.printView && locationElements,
// Is feature info visible
!viewState.featureInfoPanelIsCollapsed &&
viewState.featureInfoPanelIsVisible ? (
// Are picked features loading -> show Loader
isDefined(terria.pickedFeatures) &&
terria.pickedFeatures.isLoading ? ( // Do we have no features/catalog items to show?
_jsx("li", { children: _jsx(Loader, { light: true }) })) : featureInfoCatalogItems.length === 0 ? (_jsx("li", { className: Styles.noResults, children: this.getMessageForNoResults() })) : (
// Finally show feature info
featureInfoCatalogItems)) : null, !this.props.printView && locationElements,
// Add "filter by location" buttons if supported
filterableCatalogItems.map((pair) => TimeFilterMixin.isMixedInto(pair.catalogItem) &&
pair.feature ? (_jsx("button", { type: "button", onClick: this.filterIntervalsByFeature.bind(this, pair.catalogItem, pair.feature), className: Styles.satelliteSuggestionBtn, children: t("featureInfo.satelliteSuggestionBtn", {
catalogItemName: pair.catalogItem.name
}) }, pair.catalogItem.uniqueId)) : null)] })] }) }));
}
};
__decorate([
action.bound
], FeatureInfoPanel.prototype, "close", null);
__decorate([
action.bound
], FeatureInfoPanel.prototype, "toggleCollapsed", null);
__decorate([
action.bound
], FeatureInfoPanel.prototype, "toggleOpenFeature", null);
FeatureInfoPanel = __decorate([
observer
], FeatureInfoPanel);
function getFeatureMapByCatalogItems(terria) {
const featureMap = new Map();
const catalogItems = new Set(); // Will contain a list of all unique catalog items.
if (!isDefined(terria.pickedFeatures)) {
return { featureMap, catalogItems: Array.from(catalogItems) };
}
terria.pickedFeatures.features.forEach((feature) => {
const catalogItem = determineCatalogItem(terria.workbench, feature);
if (catalogItem?.uniqueId) {
catalogItems.add(catalogItem);
if (featureMap.has(catalogItem.uniqueId))
featureMap.get(catalogItem.uniqueId)?.push(feature);
else
featureMap.set(catalogItem.uniqueId, [feature]);
}
});
return { featureMap, catalogItems: Array.from(catalogItems) };
}
export function determineCatalogItem(workbench, feature) {
if (MappableMixin.isMixedInto(feature._catalogItem) &&
workbench.items.includes(feature._catalogItem)) {
return feature._catalogItem;
}
// Expand child members of composite catalog items.
// This ensures features from each child model are treated as belonging to
// that child model, not the parent composite model.
const items = flatten(workbench.items.map(recurseIntoMembers)).filter(MappableMixin.isMixedInto);
return items.find((item) => featureBelongsToCatalogItem(feature, item));
}
function recurseIntoMembers(catalogItem) {
if (catalogItem instanceof CompositeCatalogItem) {
return flatten(catalogItem.memberModels.map(recurseIntoMembers));
}
return [catalogItem];
}
export { FeatureInfoPanel };
export default withTranslation()(withViewState(FeatureInfoPanel));
//# sourceMappingURL=FeatureInfoPanel.js.map