terriajs
Version:
Geospatial data visualization platform.
257 lines (226 loc) • 7.8 kB
text/typescript
import i18next from "i18next";
import { computed, makeObservable, observable, runInAction } from "mobx";
import Rectangle from "terriajs-cesium/Source/Core/Rectangle";
import TerrainProvider from "terriajs-cesium/Source/Core/TerrainProvider";
import DataSource from "terriajs-cesium/Source/DataSources/DataSource";
import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset";
import ImageryProvider from "terriajs-cesium/Source/Scene/ImageryProvider";
import AbstractConstructor from "../Core/AbstractConstructor";
import AsyncLoader from "../Core/AsyncLoader";
import Result from "../Core/Result";
import Model from "../Models/Definition/Model";
import MappableTraits from "../Traits/TraitsClasses/MappableTraits";
import CatalogMemberMixin, { getName } from "./CatalogMemberMixin";
// Unfortunately Cesium does not declare a single interface that represents a primitive,
// but here is what primitives have in common:
export interface AbstractPrimitive {
show: boolean;
destroy(): void;
isDestroyed(): boolean;
}
export type MapItem =
| ImageryParts
| DataSource
| AbstractPrimitive
| TerrainProvider;
export class ImageryParts {
imageryProvider: ImageryProvider | undefined = undefined;
alpha: number = 0.8;
clippingRectangle: Rectangle | undefined = undefined;
show: boolean = true;
static fromAsync(options: {
imageryProviderPromise: Promise<ImageryProvider | undefined>;
alpha?: number;
clippingRectangle?: Rectangle;
show?: boolean;
}): ImageryParts {
const result = new ImageryParts({
imageryProvider: undefined,
alpha: options.alpha,
clippingRectangle: options.clippingRectangle,
show: options.show
});
options.imageryProviderPromise.then((imageryProvider) => {
if (imageryProvider) {
runInAction(() => {
result.imageryProvider = imageryProvider;
});
}
});
return result;
}
constructor(options: {
imageryProvider: ImageryProvider | undefined;
alpha?: number;
clippingRectangle?: Rectangle;
show?: boolean;
}) {
this.imageryProvider = options.imageryProvider;
this.alpha = options.alpha ?? 0.8;
this.clippingRectangle = options.clippingRectangle;
this.show = options.show ?? true;
}
}
// This discriminator only discriminates between ImageryParts and DataSource
export namespace ImageryParts {
export function is(object: MapItem): object is ImageryParts {
return "imageryProvider" in object;
}
}
export function isPrimitive(mapItem: MapItem): mapItem is AbstractPrimitive {
return "isDestroyed" in mapItem;
}
export function isCesium3DTileset(
mapItem: MapItem
): mapItem is Cesium3DTileset {
return "allTilesLoaded" in mapItem;
}
export function isTerrainProvider(
mapItem: MapItem
): mapItem is TerrainProvider {
return "hasVertexNormals" in mapItem;
}
export function isDataSource(object: MapItem): object is DataSource {
return "entities" in object;
}
type BaseType = Model<MappableTraits>;
function MappableMixin<T extends AbstractConstructor<BaseType>>(Base: T) {
abstract class MappableMixin extends Base {
initialMessageShown: boolean = false;
constructor(...args: any[]) {
super(...args);
makeObservable(this);
}
get isMappable() {
return true;
}
get cesiumRectangle() {
if (
this.rectangle !== undefined &&
this.rectangle.east !== undefined &&
this.rectangle.west !== undefined &&
this.rectangle.north !== undefined &&
this.rectangle.south !== undefined
) {
return Rectangle.fromDegrees(
this.rectangle.west,
this.rectangle.south,
this.rectangle.east,
this.rectangle.north
);
}
return undefined;
}
get shouldShowInitialMessage(): boolean {
if (this.initialMessage !== undefined) {
const hasTitle =
this.initialMessage.title !== undefined &&
this.initialMessage.title !== "" &&
this.initialMessage.title !== null;
const hasContent =
this.initialMessage.content !== undefined &&
this.initialMessage.content !== "" &&
this.initialMessage.content !== null;
return (hasTitle || hasContent) && !this.initialMessageShown;
}
return false;
}
private _mapItemsLoader = new AsyncLoader(
this.forceLoadMapItems.bind(this)
);
get loadMapItemsResult() {
return this._mapItemsLoader.result;
}
/**
* Gets a value indicating whether map items are currently loading.
*/
get isLoadingMapItems(): boolean {
return this._mapItemsLoader.isLoading;
}
/**
* Loads the map items. It is safe to call this as often as necessary.
* This will also call `loadMetadata()`.
* If the map items are already loaded or already loading, it will
* return the existing promise.
*
* This returns a Result object, it will contain errors if they occur - they will not be thrown.
* To throw errors, use `(await loadMetadata()).throwIfError()`
*
* {@see AsyncLoader}
*/
async loadMapItems(force?: boolean): Promise<Result<void>> {
try {
if (CatalogMemberMixin.isMixedInto(this))
(await this.loadMetadata()).throwIfError();
(await this._mapItemsLoader.load(force)).throwIfError();
} catch (e) {
return Result.error(e, {
message: `Failed to load \`${getName(this)}\` mapItems`,
importance: -1
});
}
return Result.none();
}
/**
* Forces load of the maps items. This method does _not_ need to consider
* whether the map items are already loaded.
*
* It is guaranteed that `loadMetadata` has finished before this is called.
*
* You **can not** make changes to observables until **after** an asynchronous call {@see AsyncLoader}.
*
* Errors can be thrown here.
*
* {@see AsyncLoader}
*/
protected abstract forceLoadMapItems(): Promise<void>;
/**
* Array of MapItems to show on the map/chart when Catalog Member is shown
*/
abstract get mapItems(): MapItem[];
showInitialMessage(): Promise<void> {
// This function is deliberately not a computed,
// this.terria.notificationState.addNotificationToQueue changes state
this.initialMessageShown = true;
return new Promise((resolve) => {
this.terria.notificationState.addNotificationToQueue({
title: this.initialMessage.title ?? i18next.t("notification.title"),
width: this.initialMessage.width,
height: this.initialMessage.height,
confirmText: this.initialMessage.confirmation
? this.initialMessage.confirmText
: undefined,
message: this.initialMessage.content ?? "",
key: "initialMessage:" + this.initialMessage.key,
confirmAction: () => resolve(),
showAsToast: this.initialMessage.showAsToast,
toastVisibleDuration: this.initialMessage.toastVisibleDuration
});
// No need to wait for confirmation if the message is a toast
if (this.initialMessage.showAsToast) {
resolve();
}
});
}
dispose() {
super.dispose();
this._mapItemsLoader.dispose();
}
}
return MappableMixin;
}
namespace MappableMixin {
export interface Instance extends InstanceType<
ReturnType<typeof MappableMixin>
> {}
export function isMixedInto(model: any): model is Instance {
return (
model &&
model.isMappable &&
"forceLoadMapItems" in model &&
typeof model.forceLoadMapItems === "function"
);
}
}
export default MappableMixin;