UNPKG

terriajs

Version:

Geospatial data visualization platform.

306 lines (273 loc) 8.83 kB
import i18next from "i18next"; import { action, computed, observable, makeObservable } from "mobx"; import filterOutUndefined from "../Core/filterOutUndefined"; import Result from "../Core/Result"; import TerriaError, { TerriaErrorSeverity } from "../Core/TerriaError"; import CatalogMemberMixin, { getName } from "../ModelMixins/CatalogMemberMixin"; import ChartableMixin from "../ModelMixins/ChartableMixin"; import MappableMixin from "../ModelMixins/MappableMixin"; import ReferenceMixin from "../ModelMixins/ReferenceMixin"; import TimeFilterMixin from "../ModelMixins/TimeFilterMixin"; import LayerOrderingTraits from "../Traits/TraitsClasses/LayerOrderingTraits"; import CommonStrata from "./Definition/CommonStrata"; import hasTraits from "./Definition/hasTraits"; import { BaseModel } from "./Definition/Model"; import MappableTraits from "../Traits/TraitsClasses/MappableTraits"; const keepOnTop = (model: BaseModel) => hasTraits(model, LayerOrderingTraits, "keepOnTop") && model.keepOnTop; const supportsReordering = (model: BaseModel) => hasTraits(model, LayerOrderingTraits, "supportsReordering") && model.supportsReordering; export default class Workbench { private readonly _items = observable.array<BaseModel>(); constructor() { makeObservable(this); } /** * Gets or sets the list of items on the workbench. */ @computed get items(): readonly BaseModel[] { return this._items.map(dereferenceModel); } set items(items: readonly BaseModel[]) { // Run items through a set to remove duplicates. const setItems = new Set(items); this._items.spliceWithArray( 0, this._items.length, Array.from(setItems).slice() ); } /** * Gets the unique IDs of the items in the workbench. */ @computed get itemIds(): readonly string[] { return filterOutUndefined(this._items.map((item) => item.uniqueId)); } /** * Gets the unique IDs of the items in the workbench. */ @computed get shouldExpandAll(): boolean { return this.items.every((item) => !(item as any).isOpenInWorkbench); } /** * Checks if the workbench contains time-based WMS */ @computed get hasTimeWMS(): boolean { return this._items.some( (item) => item.type === "wms" && TimeFilterMixin.isMixedInto(item) && item.discreteTimesAsSortedJulianDates?.length ); } /** * Removes a model or its dereferenced equivalent from the workbench. * @param item The model. */ @action remove(item: BaseModel): void { const index = this.indexOf(item); if (index >= 0) { this._items.splice(index, 1); } } /** * Removes all models from the workbench. */ @action removeAll(): void { this._items.clear(); } /** * Collapses all models from the workbench. */ @action collapseAll(): void { this.items.map((item) => { item.setTrait(CommonStrata.user, "isOpenInWorkbench", false); }); } /** * Expands all models from the workbench. */ @action expandAll(): void { this.items.map((item) => { item.setTrait(CommonStrata.user, "isOpenInWorkbench", true); }); } /** * Disable all items in the workbench. */ @action disableAll() { this.items.forEach((item) => { if (hasTraits(item, MappableTraits, "show")) { item.setTrait(CommonStrata.user, "show", false); } }); } @action enableAll() { this.items.forEach((item) => { if (hasTraits(item, MappableTraits, "show")) { item.setTrait(CommonStrata.user, "show", true); } }); } /** * Adds an item to the workbench. If the item is already present, this method does nothing. * Note that the model's dereferenced equivalent may appear in the {@link Workbench#items} list * rather than the model itself. * @param item The model to add. */ @action private insertItem(item: BaseModel, index: number = 0) { if (this.contains(item)) { return; } const targetItem: BaseModel = dereferenceModel(item); // Keep reorderable data sources (e.g.: imagery layers) below non-orderable ones (e.g.: GeoJSON). if (supportsReordering(targetItem)) { while ( index < this.items.length && !supportsReordering(this.items[index]) ) { ++index; } } else { while ( index > 0 && this.items.length > 0 && supportsReordering(this.items[index - 1]) ) { --index; } } if (!keepOnTop(targetItem)) { while ( index < this.items.length && keepOnTop(this.items[index]) && supportsReordering(this.items[index]) === supportsReordering(targetItem) ) { ++index; } } else { while ( index > 0 && this.items.length > 0 && !keepOnTop(this.items[index - 1]) && supportsReordering(this.items[index - 1]) === supportsReordering(targetItem) ) { --index; } } // Make sure the reference, rather than the target, is added to the items list. const referenceItem = item.sourceReference ? item.sourceReference : item; this._items.splice(index, 0, referenceItem); } /** * Adds or removes a model to/from the workbench. If the model is a reference, * it will also be dereferenced. If, after dereferencing, the item turns out not to * be {@link AsyncMappableMixin} or {@link ChartableMixin} but it is a {@link GroupMixin}, it will * be removed from the workbench. If it is mappable, `loadMapItems` will be called. * * If an error occurs, it will only be added to the workbench if the severity is TerriaError.Warning - otherwise it will not be added * * @param item The item to add to or remove from the workbench. */ public async add(item: BaseModel | BaseModel[]): Promise<Result<unknown>> { if (Array.isArray(item)) { const results = await Promise.all(item.reverse().map((i) => this.add(i))); return Result.combine(results, { title: i18next.t("workbench.addItemErrorTitle"), message: i18next.t("workbench.addItemErrorMessage"), importance: -1 }); } if (MappableMixin.isMixedInto(item) && item.shouldShowInitialMessage) { await item.showInitialMessage(); } this.insertItem(item); let error: TerriaError | undefined; if (ReferenceMixin.isMixedInto(item)) { error = (await item.loadReference()).error; if (item.target) { this.remove(item); return this.add(item.target); } } // Add warning message if item isn't mappable or chartable if ( !error && !MappableMixin.isMixedInto(item) && !ChartableMixin.isMixedInto(item) ) { error = TerriaError.from( `${getName(item)} doesn't have anything to visualize`, TerriaErrorSeverity.Warning ); } if (!error && CatalogMemberMixin.isMixedInto(item)) error = (await item.loadMetadata()).error; if (!error && MappableMixin.isMixedInto(item)) { error = (await item.loadMapItems()).error; if (!error && item.zoomOnAddToWorkbench && !item.disableZoomTo) { item.terria.currentViewer.zoomTo(item); } } // Remove item if TerriaError severity is Error if (error?.severity === TerriaErrorSeverity.Error) { this.remove(item); } return Result.none(error, { title: i18next.t("workbench.addItemErrorTitle"), message: i18next.t("workbench.addItemErrorMessage"), importance: -1 }); } /** * Determines if a given model or its dereferenced equivalent exists in the workbench list. * @param item The model. * @returns True if the model or its dereferenced equivalent exists on the workbench; otherwise, false. */ contains(item: BaseModel): boolean { return this.indexOf(item) >= 0; } /** * Returns the index of a given model or its dereferenced equivalent in the workbench list. * @param item The model. * @returns The index of the model or its dereferenced equivalent, or -1 if neither exist on the workbench. */ indexOf(item: BaseModel): number { return this.items.findIndex( (model) => model === item || dereferenceModel(model) === dereferenceModel(item) ); } /** * Used to re-order the workbench list. * @param item The model to be moved. * @param newIndex The new index to shift the model to. */ @action moveItemToIndex(item: BaseModel, newIndex: number): void { if (!this.contains(item)) { return; } this.remove(item); this.insertItem(item, newIndex); } } function dereferenceModel(model: BaseModel): BaseModel { if (ReferenceMixin.isMixedInto(model) && model.target !== undefined) { return model.target; } return model; }