UNPKG

terriajs

Version:

Geospatial data visualization platform.

349 lines (311 loc) 14.3 kB
'use strict'; import defined from 'terriajs-cesium/Source/Core/defined'; import CesiumMath from 'terriajs-cesium/Source/Core/Math'; import Ellipsoid from 'terriajs-cesium/Source/Core/Ellipsoid'; import FeatureInfoCatalogItem from './FeatureInfoCatalogItem.jsx'; import Loader from '../Loader.jsx'; import ObserveModelMixin from '../ObserveModelMixin'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import knockout from 'terriajs-cesium/Source/ThirdParty/knockout'; import Entity from 'terriajs-cesium/Source/DataSources/Entity'; import Icon from "../Icon.jsx"; import { LOCATION_MARKER_DATA_SOURCE_NAME, addMarker, removeMarker, markerVisible } from '../../Models/LocationMarkerUtils'; import prettifyCoordinates from '../../Map/prettifyCoordinates'; import Styles from './feature-info-panel.scss'; import classNames from 'classnames'; const FeatureInfoPanel = createReactClass({ displayName: 'FeatureInfoPanel', mixins: [ObserveModelMixin], propTypes: { terria: PropTypes.object.isRequired, viewState: PropTypes.object.isRequired, printView: PropTypes.bool }, componentDidMount() { const createFakeSelectedFeatureDuringPicking = true; const terria = this.props.terria; this._pickedFeaturesSubscription = knockout.getObservable(terria, 'pickedFeatures').subscribe(() => { const pickedFeatures = terria.pickedFeatures; if (!defined(pickedFeatures)) { terria.selectedFeature = undefined; } else { if (createFakeSelectedFeatureDuringPicking) { const fakeFeature = new Entity({ id: 'Pick Location' }); fakeFeature.position = pickedFeatures.pickPosition; terria.selectedFeature = fakeFeature; } else { terria.selectedFeature = undefined; } if (defined(pickedFeatures.allFeaturesAvailablePromise)) { pickedFeatures.allFeaturesAvailablePromise.then(() => { // 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 => defined(determineCatalogItem(terria.nowViewing, x))); terria.selectedFeature = featuresShownAtAll.filter(featureHasInfo)[0]; if (!defined(terria.selectedFeature) && (featuresShownAtAll.length > 0)) { // Handles the case when no features have info - still want something to be open. terria.selectedFeature = featuresShownAtAll[0]; } }); } } }); }, componentWillUnmount() { if (defined(this._pickedFeaturesSubscription)) { this._pickedFeaturesSubscription.dispose(); this._pickedFeaturesSubscription = undefined; } }, getFeatureInfoCatalogItems() { const {catalogItems, featureCatalogItemPairs} = getFeaturesGroupedByCatalogItems(this.props.terria); return catalogItems .filter(catalogItem => defined(catalogItem)) .map((catalogItem, i) => { // From the pairs, select only those with this catalog item, and pull the features out of the pair objects. const features = featureCatalogItemPairs.filter(pair => pair.catalogItem === catalogItem).map(pair => pair.feature); return ( <FeatureInfoCatalogItem key={i} viewState={this.props.viewState} catalogItem={catalogItem} features={features} terria={this.props.terria} onToggleOpen={this.toggleOpenFeature} printView={this.props.printView} /> ); }); }, close() { this.props.viewState.featureInfoPanelIsVisible = false; // give the close animation time to finish before unselecting, to avoid jumpiness setTimeout(() => { this.props.terria.pickedFeatures = undefined; this.props.terria.selectedFeature = undefined; }, 200); }, toggleCollapsed() { this.props.viewState.featureInfoPanelIsCollapsed = !this.props.viewState.featureInfoPanelIsCollapsed; }, toggleOpenFeature(feature) { const terria = this.props.terria; if (feature === terria.selectedFeature) { terria.selectedFeature = undefined; } else { terria.selectedFeature = feature; } }, getMessageForNoResults() { if (this.props.terria.nowViewing.hasItems) { // feature info shows up becuase data has been added for the first time if (this.props.viewState.firstTimeAddingData) { this.props.viewState.firstTimeAddingData = false; return "Click on the map to learn more about a location"; } // if clicking on somewhere that has no data return "No data is available here - try another location."; } else { return "Click 'Add Data' to add data to the map."; } }, addManualMarker(longitude, latitude) { addMarker(this.props.terria, { name: "User Selection", location: { latitude: latitude, longitude: longitude } }); }, pinClicked(longitude, latitude) { if (!markerVisible(this.props.terria)) { this.addManualMarker(longitude, latitude); } else { removeMarker(this.props.terria); } }, locationUpdated(longitude, latitude) { if (defined(latitude) && defined (longitude) && markerVisible(this.props.terria)) { removeMarker(this.props.terria); this.addManualMarker(longitude, latitude); } }, renderLocationItem(cartesianPosition) { const catographic = Ellipsoid.WGS84.cartesianToCartographic(cartesianPosition); const latitude = CesiumMath.toDegrees(catographic.latitude); const longitude = CesiumMath.toDegrees(catographic.longitude); const pretty = prettifyCoordinates(longitude, latitude); this.locationUpdated(longitude, latitude); const that = this; const pinClicked = function() { that.pinClicked(longitude, latitude); }; const locationButtonStyle = markerVisible(this.props.terria) ? Styles.btnLocationSelected : Styles.btnLocation; return ( <div className={Styles.location}> <span>Lat / Lon&nbsp;</span> <span> {pretty.latitude + ", " + pretty.longitude} {!this.props.printView && <button type='button' onClick={pinClicked} className={locationButtonStyle}> <Icon glyph={Icon.GLYPHS.location}/> </button>} </span> </div> ); }, render() { const terria = this.props.terria; const viewState = this.props.viewState; const featureInfoCatalogItems = this.getFeatureInfoCatalogItems(); const panelClassName = classNames(Styles.panel, { [Styles.isCollapsed]: viewState.featureInfoPanelIsCollapsed, [Styles.isVisible]: viewState.featureInfoPanelIsVisible }); let position; if (defined(terria.selectedFeature) && defined(terria.selectedFeature.position)) { // If the clock is avaliable then use it, otherwise don't. let clock; if (defined(terria.clock)) { clock = terria.clock.currentTime; } // If there is a selected feature then use the feature location. 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 (isNaN(position.x) || isNaN(position.y) || isNaN(position.z)) { position = undefined; } } if (!defined(position)) { // Otherwise use the location picked. if (defined(terria.pickedFeatures) && defined(terria.pickedFeatures.pickPosition)) { position = terria.pickedFeatures.pickPosition; } } const locationElements = ( <If condition={position}> <li>{this.renderLocationItem(position)}</li> </If> ); return ( <div className={panelClassName} aria-hidden={!viewState.featureInfoPanelIsVisible}> {!this.props.printView && <div className={Styles.header}> <button type='button' onClick={ this.toggleCollapsed } className={Styles.btnPanelHeading}> Feature Information </button> <button type='button' onClick={ this.close } className={Styles.btnCloseFeature} title="Close data panel"> <Icon glyph={Icon.GLYPHS.close}/> </button> </div>} <ul className={Styles.body}> {this.props.printView && locationElements} <Choose> <When condition={viewState.featureInfoPanelIsCollapsed || !viewState.featureInfoPanelIsVisible}> </When> <When condition={defined(terria.pickedFeatures) && terria.pickedFeatures.isLoading}> <li><Loader/></li> </When> <When condition={!featureInfoCatalogItems || featureInfoCatalogItems.length === 0}> <li className={Styles.noResults}>{this.getMessageForNoResults()}</li> </When> <Otherwise> {featureInfoCatalogItems} </Otherwise> </Choose> {!this.props.printView && locationElements} </ul> </div> ); }, }); /** * Returns an object of {catalogItems, featureCatalogItemPairs}. */ function getFeaturesGroupedByCatalogItems(terria) { if (!defined(terria.pickedFeatures)) { return {catalogItems: [], featureCatalogItemPairs: []}; } const features = terria.pickedFeatures.features; const featureCatalogItemPairs = []; // Will contain objects of {feature, catalogItem}. const catalogItems = []; // Will contain a list of all unique catalog items. features.forEach(feature => { // Why was this here? Surely changing the feature objects is not a good side-effect? // if (!defined(feature.position)) { // feature.position = terria.pickedFeatures.pickPosition; // } const catalogItem = determineCatalogItem(terria.nowViewing, feature); featureCatalogItemPairs.push({ catalogItem: catalogItem, feature: feature }); if (catalogItems.indexOf(catalogItem) === -1) { // Note this works for undefined too. catalogItems.push(catalogItem); } }); return {catalogItems, featureCatalogItemPairs}; } /** * Figures out what the catalog item for a feature is. * * @param nowViewing {@link NowViewing} to look in the items for. * @param feature Feature to match * @returns {CatalogItem} */ function determineCatalogItem(nowViewing, feature) { if (!defined(nowViewing)) { // So that specs do not need to define a nowViewing. return undefined; } // "Data sources" (eg. czml, geojson, kml, csv) have an entity collection defined on the entity // (and therefore the feature). // Then match up the data source on the feature with a now-viewing item's data source. // // Gpx, Ogr, WebFeatureServiceCatalogItem, ArcGisFeatureServerCatalogItem, WebProcessingServiceCatalogItem // all have a this._geoJsonItem, which we also need to check. let result; let i; let item; if (defined(feature.entityCollection) && defined(feature.entityCollection.owner)) { const dataSource = feature.entityCollection.owner; if (dataSource.name === LOCATION_MARKER_DATA_SOURCE_NAME) { return { name: 'Location Marker' }; } for (i = nowViewing.items.length - 1; i >= 0; i--) { item = nowViewing.items[i]; if (item.dataSource === dataSource) { result = item; break; } } return result; } // If there is no data source, but there is an imagery layer (eg. ArcGIS), // we can match up the imagery layer on the feature with a now-viewing item. if (defined(feature.imageryLayer)) { const imageryLayer = feature.imageryLayer; for (i = nowViewing.items.length - 1; i >= 0; i--) { if (nowViewing.items[i].imageryLayer === imageryLayer) { result = nowViewing.items[i]; break; } } return result; } } /** * Determines whether the passed feature has properties or a description. */ function featureHasInfo(feature) { return (defined(feature.properties) || defined(feature.description)); } module.exports = FeatureInfoPanel;