UNPKG

terriajs

Version:

Geospatial data visualization platform.

257 lines (226 loc) 7.8 kB
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 { @observable 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; } @computed 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;