UNPKG

terriajs

Version:

Geospatial data visualization platform.

450 lines (392 loc) 16.7 kB
import { action, makeObservable, observable, runInAction } from "mobx"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Color from "terriajs-cesium/Source/Core/Color"; import createGuid from "terriajs-cesium/Source/Core/createGuid"; import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import ColorMaterialProperty from "terriajs-cesium/Source/DataSources/ColorMaterialProperty"; import ConstantPositionProperty from "terriajs-cesium/Source/DataSources/ConstantPositionProperty"; import ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantProperty"; import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import isDefined from "../Core/isDefined"; import LatLonHeight from "../Core/LatLonHeight"; import TerriaError from "../Core/TerriaError"; import ProtomapsImageryProvider from "../Map/ImageryProvider/ProtomapsImageryProvider"; import featureDataToGeoJson from "../Map/PickedFeatures/featureDataToGeoJson"; import { ProviderCoordsMap } from "../Map/PickedFeatures/PickedFeatures"; import MappableMixin from "../ModelMixins/MappableMixin"; import TimeVarying from "../ModelMixins/TimeVarying"; import MouseCoords from "../ReactViewModels/MouseCoords"; import TableColorStyleTraits from "../Traits/TraitsClasses/Table/ColorStyleTraits"; import TableOutlineStyleTraits, { OutlineSymbolTraits } from "../Traits/TraitsClasses/Table/OutlineStyleTraits"; import TableStyleTraits from "../Traits/TraitsClasses/Table/StyleTraits"; import CameraView from "./CameraView"; import CommonStrata from "./Definition/CommonStrata"; import createStratumInstance from "./Definition/createStratumInstance"; import TerriaFeature from "./Feature/Feature"; import Terria from "./Terria"; import HighlightColorTraits from "../Traits/TraitsClasses/HighlightColorTraits"; import hasTraits from "./Definition/hasTraits"; import "./Feature/ImageryLayerFeatureInfo"; // overrides Cesium's prototype.configureDescriptionFromProperties export default abstract class GlobeOrMap { abstract readonly type: string; abstract readonly terria: Terria; abstract readonly canShowSplitter: boolean; public static featureHighlightID = "___$FeatureHighlight&__"; protected static _featureHighlightName = "TerriaJS Feature Highlight Marker"; private _removeHighlightCallback?: () => Promise<void> | void; private _highlightPromise: Promise<unknown> | undefined; private _tilesLoadingCountMax: number = 0; protected supportsPolylinesOnTerrain?: boolean; // True if zoomTo() was called and the map is currently zooming to dataset @observable isMapZooming = false; // An internal id to track an in progress call to zoomTo() _currentZoomId?: string; // This is updated by Leaflet and Cesium objects. // Avoid duplicate mousemove events. Why would we get duplicate mousemove events? I'm glad you asked: // http://stackoverflow.com/questions/17818493/mousemove-event-repeating-every-second/17819113 // I (Kevin Ring) see this consistently on my laptop when Windows Media Player is running. mouseCoords: MouseCoords = new MouseCoords(); abstract destroy(): void; abstract doZoomTo( target: CameraView | Rectangle | MappableMixin.Instance, flightDurationSeconds: number ): Promise<void>; constructor() { makeObservable(this); } /** * Zoom map to a dataset or the given bounds. * * @param target A bounds item to zoom to * @param flightDurationSeconds Optional time in seconds for the zoom animation to complete * @returns A promise that resolves when the zoom animation is complete */ @action zoomTo( target: CameraView | Rectangle | MappableMixin.Instance, flightDurationSeconds: number = 3.0 ): Promise<void> { this.isMapZooming = true; const zoomId = createGuid(); this._currentZoomId = zoomId; return this.doZoomTo(target, flightDurationSeconds).finally( action(() => { // Unset isMapZooming only if the local zoomId matches _currentZoomId. // If they do not match, it means there was another call to zoomTo which // could still be in progress and it will handle unsetting isMapZooming. if (zoomId === this._currentZoomId) { this.isMapZooming = false; this._currentZoomId = undefined; if (MappableMixin.isMixedInto(target) && TimeVarying.is(target)) { // Set the target as the source for timeline this.terria.timelineStack.promoteToTop(target); } } }) ); } /** * Set initial camera view */ abstract setInitialView(cameraView: CameraView): void; abstract getCurrentCameraView(): CameraView; /* Gets the current container element. */ abstract getContainer(): Element | undefined; abstract pauseMapInteraction(): void; abstract resumeMapInteraction(): void; abstract notifyRepaintRequired(): void; /** * List of the attributions (credits) for data currently displayed on map. */ get attributions(): string[] { return []; } /** * Picks features based off a latitude, longitude and (optionally) height. * @param latLngHeight The position on the earth to pick. * @param providerCoords A map of imagery provider urls to the coords used to get features for those imagery * providers - i.e. x, y, level * @param existingFeatures An optional list of existing features to concatenate the ones found from asynchronous picking to. */ abstract pickFromLocation( latLngHeight: LatLonHeight, providerCoords: ProviderCoordsMap, existingFeatures: TerriaFeature[] ): void; /** * Creates a {@see Feature} (based on an {@see Entity}) from a {@see ImageryLayerFeatureInfo}. * @param imageryFeature The imagery layer feature for which to create an entity-based feature. * @return The created feature. */ protected _createFeatureFromImageryLayerFeature( imageryFeature: ImageryLayerFeatureInfo ): TerriaFeature { const feature = new TerriaFeature({ id: imageryFeature.name }); feature.name = imageryFeature.name; if (imageryFeature.description) { feature.description = new ConstantProperty(imageryFeature.description); // already defined by the new Entity } feature.properties = imageryFeature.properties; feature.data = imageryFeature.data; feature.imageryLayer = imageryFeature.imageryLayer; if (imageryFeature.position) { feature.position = new ConstantPositionProperty( Ellipsoid.WGS84.cartographicToCartesian(imageryFeature.position) ); } (feature as any).coords = (imageryFeature as any).coords; return feature; } /** * Adds loading progress for cesium */ protected _updateTilesLoadingCount(tilesLoadingCount: number): void { if (tilesLoadingCount > this._tilesLoadingCountMax) { this._tilesLoadingCountMax = tilesLoadingCount; } else if (tilesLoadingCount === 0) { this._tilesLoadingCountMax = 0; } this.terria.tileLoadProgressEvent.raiseEvent( tilesLoadingCount, this._tilesLoadingCountMax ); } /** * Adds loading progress (boolean) for 3DTileset layers where total tiles is not known */ protected _updateTilesLoadingIndeterminate(loading: boolean): void { this.terria.indeterminateTileLoadProgressEvent.raiseEvent(loading); } /** * Returns the side of the splitter the `position` lies on. * * @param The screen position. * @return The side of the splitter on which `position` lies. */ protected _getSplitterSideForScreenPosition( position: Cartesian2 | Cartesian3 ): SplitDirection | undefined { const container = this.terria.currentViewer.getContainer(); if (!isDefined(container)) { return; } const splitterX = container.clientWidth * this.terria.splitPosition; if (position.x <= splitterX) { return SplitDirection.LEFT; } else { return SplitDirection.RIGHT; } } abstract _addVectorTileHighlight( imageryProvider: ProtomapsImageryProvider, rectangle: Rectangle ): () => void; async _highlightFeature(feature: TerriaFeature | undefined): Promise<void> { if (isDefined(this._removeHighlightCallback)) { await this._removeHighlightCallback(); this._removeHighlightCallback = undefined; this._highlightPromise = undefined; } // Lazy import here to avoid cyclic dependencies. const { default: GeoJsonCatalogItem } = await import("./Catalog/CatalogItems/GeoJsonCatalogItem"); if (isDefined(feature)) { let hasGeometry = false; if (isDefined(feature._cesium3DTileFeature)) { const originalColor = feature._cesium3DTileFeature.color; const defaultColor = Color.fromCssColorString("#fffffe"); // Get the highlight color from the catalogItem trait or default to baseMapContrastColor const catalogItem = feature._catalogItem; let highlightColorString; if (hasTraits(catalogItem, HighlightColorTraits, "highlightColor")) { highlightColorString = runInAction(() => catalogItem.highlightColor); runInAction(() => catalogItem.highlightColor); } else { highlightColorString = this.terria.baseMapContrastColor; } const highlightColor: Color = isDefined(highlightColorString) ? Color.fromCssColorString(highlightColorString) : defaultColor; // highlighting doesn't work if the highlight colour is full white // so in this case use something close to white instead feature._cesium3DTileFeature.color = Color.equals( highlightColor, Color.WHITE ) ? defaultColor : highlightColor; this._removeHighlightCallback = function () { if ( isDefined(feature._cesium3DTileFeature) && feature._cesium3DTileFeature.tileset.isDestroyed() === false ) { try { feature._cesium3DTileFeature.color = originalColor; } catch (err) { TerriaError.from(err).log(); } } }; } else if (isDefined(feature.polygon)) { hasGeometry = true; const cesiumPolygon = feature.cesiumEntity || feature; const polygonOutline = cesiumPolygon.polygon!.outline; const polygonOutlineColor = cesiumPolygon.polygon!.outlineColor; const polygonMaterial = cesiumPolygon.polygon!.material; cesiumPolygon.polygon!.outline = new ConstantProperty(true); cesiumPolygon.polygon!.outlineColor = new ConstantProperty( Color.fromCssColorString(this.terria.baseMapContrastColor) ?? Color.GRAY ); cesiumPolygon.polygon!.material = new ColorMaterialProperty( new ConstantProperty( ( Color.fromCssColorString(this.terria.baseMapContrastColor) ?? Color.LIGHTGRAY ).withAlpha(0.75) ) ); this._removeHighlightCallback = function () { if (cesiumPolygon.polygon) { cesiumPolygon.polygon.outline = polygonOutline; cesiumPolygon.polygon.outlineColor = polygonOutlineColor; cesiumPolygon.polygon.material = polygonMaterial; } }; } else if (isDefined(feature.polyline)) { hasGeometry = true; const cesiumPolyline = feature.cesiumEntity || feature; const polylineMaterial = cesiumPolyline.polyline!.material; const polylineWidth = cesiumPolyline.polyline!.width; (cesiumPolyline as any).polyline.material = Color.fromCssColorString(this.terria.baseMapContrastColor) ?? Color.LIGHTGRAY; cesiumPolyline.polyline!.width = new ConstantProperty(2); this._removeHighlightCallback = function () { if (cesiumPolyline.polyline) { cesiumPolyline.polyline.material = polylineMaterial; cesiumPolyline.polyline.width = polylineWidth; } }; } if (!hasGeometry) { let vectorTileHighlightCreated = false; // Feature from ProtomapsImageryProvider if ( feature.imageryLayer?.imageryProvider instanceof ProtomapsImageryProvider ) { const highlightImageryProvider = feature.imageryLayer.imageryProvider.createHighlightImageryProvider( feature ); if (highlightImageryProvider) this._removeHighlightCallback = this.terria.currentViewer._addVectorTileHighlight( highlightImageryProvider, feature.imageryLayer.imageryProvider.rectangle ); vectorTileHighlightCreated = true; } // No vector tile highlight was created so try to convert feature to GeoJSON // This flag is necessary to check as it is possible for a feature to use ProtomapsImageryProvider and also have GeoJson data - but maybe failed to createHighlightImageryProvider if (!vectorTileHighlightCreated) { const geoJson = featureDataToGeoJson(feature.data); // Don't show points; the targeting cursor is sufficient. if (geoJson) { geoJson.features = geoJson.features.filter( (f) => f.geometry.type !== "Point" ); let catalogItem = this.terria.getModelById( GeoJsonCatalogItem, GlobeOrMap.featureHighlightID ); if (catalogItem === undefined) { catalogItem = new GeoJsonCatalogItem( GlobeOrMap.featureHighlightID, this.terria ); catalogItem.setTrait( CommonStrata.definition, "name", GlobeOrMap._featureHighlightName ); this.terria.addModel(catalogItem); } catalogItem.setTrait( CommonStrata.user, "geoJsonData", geoJson as any ); catalogItem.setTrait( CommonStrata.user, "useOutlineColorForLineFeatures", true ); catalogItem.setTrait( CommonStrata.user, "defaultStyle", createStratumInstance(TableStyleTraits, { outline: createStratumInstance(TableOutlineStyleTraits, { null: createStratumInstance(OutlineSymbolTraits, { width: 4, color: this.terria.baseMapContrastColor }) }), color: createStratumInstance(TableColorStyleTraits, { nullColor: "rgba(0,0,0,0)" }) }) ); this.terria.overlays.add(catalogItem); this._highlightPromise = catalogItem.loadMapItems(); const removeCallback = (this._removeHighlightCallback = () => { if (!isDefined(this._highlightPromise)) { return; } return this._highlightPromise .then(() => { if (removeCallback !== this._removeHighlightCallback) { return; } if (isDefined(catalogItem)) { catalogItem.setTrait(CommonStrata.user, "show", false); } }) .catch(function () {}); }); (await catalogItem.loadMapItems()).logError( "Error occurred while loading picked feature" ); // Check to make sure we don't have a different `catalogItem` after loading if (removeCallback !== this._removeHighlightCallback) { return; } catalogItem.setTrait(CommonStrata.user, "show", true); this._highlightPromise = this.terria.overlays .add(catalogItem) .then((r) => r.throwIfError()); } } } } } /** * Captures a screenshot of the map. * @return A promise that resolves to a data URL when the screenshot is ready. */ captureScreenshot(): Promise<string> { throw new DeveloperError( "captureScreenshot must be implemented in the derived class." ); } }