UNPKG

terriajs

Version:

Geospatial data visualization platform.

317 lines (283 loc) 9.86 kB
import { action, computed, observable, onBecomeObserved, makeObservable, override } from "mobx"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; 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 Entity from "terriajs-cesium/Source/DataSources/Entity"; import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; import ImageryProvider from "terriajs-cesium/Source/Scene/ImageryProvider"; import AbstractConstructor from "../Core/AbstractConstructor"; import filterOutUndefined from "../Core/filterOutUndefined"; import LatLonHeight from "../Core/LatLonHeight"; import runLater from "../Core/runLater"; import { ProviderCoords, ProviderCoordsMap } from "../Map/PickedFeatures/PickedFeatures"; import CommonStrata from "../Models/Definition/CommonStrata"; import createStratumInstance from "../Models/Definition/createStratumInstance"; import Model from "../Models/Definition/Model"; import TerriaFeature from "../Models/Feature/Feature"; import TimeFilterTraits, { TimeFilterCoordinates } from "../Traits/TraitsClasses/TimeFilterTraits"; import DiscretelyTimeVaryingMixin from "./DiscretelyTimeVaryingMixin"; import MappableMixin, { ImageryParts } from "./MappableMixin"; type BaseType = Model<TimeFilterTraits> & MappableMixin.Instance; /** * A Mixin for filtering the dates for which imagery is available at a location * picked by the user * * When `timeFilterPropertyName` is set, we look for a property of that name in * the feature query response at the picked location. The property value should be * an array of dates for which imagery is available at the location. This * Mixin is used to implement the Location filter feature for Satellite * Imagery. */ function TimeFilterMixin<T extends AbstractConstructor<BaseType>>(Base: T) { abstract class TimeFilterMixin extends DiscretelyTimeVaryingMixin(Base) { @observable _currentTimeFilterFeature?: Entity; constructor(...args: any[]) { super(...args); makeObservable(this); // Try to resolve the timeFilterFeature from the co-ordinates that might // be stored in the traits. We only have to resolve the time filter // feature once to get the list of times. const disposeListener = onBecomeObserved(this, "mapItems", () => { runLater( action(async () => { if (!MappableMixin.isMixedInto(this)) { disposeListener(); return; } const coords = coordinatesFromTraits(this.timeFilterCoordinates); if (coords) { this.setTimeFilterFromLocation(coords); } disposeListener(); }) ); }); } /** * Loads the time filter by querying the imagery layers at the given * co-ordinates. * * @param coordinates.position The lat, lon, height of the location to pick in degrees * @param coordinates.tileCoords the x, y, level co-ordinates of the tile to pick * @returns Promise that fulfills when the picking is complete. * The promise resolves to `false` if the filter could not be set for some reason. */ @action async setTimeFilterFromLocation(coordinates: { position: LatLonHeight; tileCoords: ProviderCoords; }): Promise<boolean> { const timeProperty = this.timeFilterPropertyName; if ( this.terria.allowFeatureInfoRequests === false || timeProperty === undefined || !MappableMixin.isMixedInto(this) ) { return false; } const pickTileCoordinates = coordinates.tileCoords; const pickCartographicPosition = Cartographic.fromDegrees( coordinates.position.longitude, coordinates.position.latitude, coordinates.position.height ); // Pick all imagery provider for this item const imageryProviders = filterOutUndefined( this.mapItems.map((mapItem) => ImageryParts.is(mapItem) ? mapItem.imageryProvider : undefined ) ); const picks = await pickImageryProviders( imageryProviders, pickTileCoordinates, pickCartographicPosition ); // Find the first imageryProvider and feature with a matching time property let timeFeature: TerriaFeature | undefined; let imageryUrl: string | undefined; for (let i = 0; i < picks.length; i++) { const pick = picks[i]; const imageryFeature = pick.imageryFeatures.find( (f) => f.properties[timeProperty] !== undefined ); imageryUrl = (pick.imageryProvider as any).url; if (imageryFeature && imageryUrl) { imageryFeature.position = imageryFeature.position ?? pickCartographicPosition; timeFeature = TerriaFeature.fromImageryLayerFeatureInfo(imageryFeature); break; } } if (!timeFeature || !imageryUrl) { return false; } // Update time filter this.setTimeFilterFeature(timeFeature, { [imageryUrl]: { ...coordinates.position, ...coordinates.tileCoords } }); return true; } get hasTimeFilterMixin() { return true; } @computed get canFilterTimeByFeature(): boolean { return this.timeFilterPropertyName !== undefined; } @computed private get imageryUrls() { if (!MappableMixin.isMixedInto(this)) return []; return filterOutUndefined( this.mapItems.map( // @ts-expect-error url attr (mapItem) => ImageryParts.is(mapItem) && mapItem.imageryProvider.url ) ); } @computed get featureTimesAsJulianDates() { if ( this._currentTimeFilterFeature === undefined || this._currentTimeFilterFeature.properties === undefined || this.timeFilterPropertyName === undefined ) { return; } const featureTimes = this._currentTimeFilterFeature.properties[ this.timeFilterPropertyName ]?.getValue(this.currentTime); if (!Array.isArray(featureTimes)) { return; } return filterOutUndefined( featureTimes.map((s) => { try { return s === undefined ? undefined : JulianDate.fromIso8601(s); } catch { return undefined; } }) ); } @override get discreteTimesAsSortedJulianDates() { const featureTimes = this.featureTimesAsJulianDates; if (featureTimes === undefined) { return super.discreteTimesAsSortedJulianDates; } return super.discreteTimesAsSortedJulianDates?.filter((dt) => featureTimes.some((d) => d.equals(dt.time)) ); } @computed get timeFilterFeature() { return this._currentTimeFilterFeature; } @action setTimeFilterFeature(feature: Entity, providerCoords?: ProviderCoordsMap) { if (!MappableMixin.isMixedInto(this) || providerCoords === undefined) return; this._currentTimeFilterFeature = feature; if (!this.currentTimeAsJulianDate) { return; } if (!feature.position) { return; } const position = feature.position.getValue(this.currentTimeAsJulianDate); if (position === undefined) return; const cartographic = Ellipsoid.WGS84.cartesianToCartographic(position); const featureImageryUrl = this.imageryUrls.find( (url) => providerCoords[url] ); const tileCoords = featureImageryUrl && providerCoords[featureImageryUrl]; if (!tileCoords) return; this.setTrait( CommonStrata.user, "timeFilterCoordinates", createStratumInstance(TimeFilterCoordinates, { tile: tileCoords, longitude: CesiumMath.toDegrees(cartographic.longitude), latitude: CesiumMath.toDegrees(cartographic.latitude), height: cartographic.height }) ); } @action removeTimeFilterFeature() { this._currentTimeFilterFeature = undefined; this.setTrait(CommonStrata.user, "timeFilterCoordinates", undefined); } } return TimeFilterMixin; } namespace TimeFilterMixin { export interface Instance extends InstanceType< ReturnType<typeof TimeFilterMixin> > {} export function isMixedInto(model: any): model is Instance { return model && model.hasTimeFilterMixin; } } /** * Picks all the imagery providers at the given coordinates and return a * promise that resolves to the (imageryProvider, imageryFeatures) pairs. */ async function pickImageryProviders( imageryProviders: ImageryProvider[], pickCoordinates: ProviderCoords, pickPosition: Cartographic ): Promise< { imageryProvider: ImageryProvider; imageryFeatures: ImageryLayerFeatureInfo[]; }[] > { return filterOutUndefined( await Promise.all( imageryProviders.map((imageryProvider) => imageryProvider .pickFeatures( pickCoordinates.x, pickCoordinates.y, pickCoordinates.level, pickPosition.longitude, pickPosition.latitude ) ?.then((imageryFeatures) => ({ imageryProvider, imageryFeatures })) ) ) ); } function coordinatesFromTraits(traits: Model<TimeFilterCoordinates>) { const { latitude, longitude, height, tile: { x, y, level } } = traits; if (latitude === undefined || longitude === undefined) return; if (x === undefined || y === undefined || level === undefined) return; return { position: { latitude, longitude, height }, tileCoords: { x, y, level } }; } export default TimeFilterMixin;