terriajs
Version:
Geospatial data visualization platform.
317 lines (283 loc) • 9.86 kB
text/typescript
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) {
_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.
*/
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;
}
get canFilterTimeByFeature(): boolean {
return this.timeFilterPropertyName !== undefined;
}
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
)
);
}
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;
}
})
);
}
get discreteTimesAsSortedJulianDates() {
const featureTimes = this.featureTimesAsJulianDates;
if (featureTimes === undefined) {
return super.discreteTimesAsSortedJulianDates;
}
return super.discreteTimesAsSortedJulianDates?.filter((dt) =>
featureTimes.some((d) => d.equals(dt.time))
);
}
get timeFilterFeature() {
return this._currentTimeFilterFeature;
}
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
})
);
}
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;