UNPKG

terriajs

Version:

Geospatial data visualization platform.

274 lines (240 loc) 8.34 kB
import { action, computed, isObservableArray, runInAction, toJS, makeObservable, override } from "mobx"; import Mustache from "mustache"; import AbstractConstructor from "../Core/AbstractConstructor"; import AsyncLoader from "../Core/AsyncLoader"; import isDefined from "../Core/isDefined"; import { isJsonObject, isJsonString, JsonObject } from "../Core/Json"; import Result from "../Core/Result"; import hasTraits from "../Models/Definition/hasTraits"; import Model, { BaseModel } from "../Models/Definition/Model"; import updateModelFromJson from "../Models/Definition/updateModelFromJson"; import SelectableDimensions, { SelectableDimension } from "../Models/SelectableDimensions/SelectableDimensions"; import ViewingControls, { ViewingControl } from "../Models/ViewingControls"; import CatalogMemberReferenceTraits from "../Traits/TraitsClasses/CatalogMemberReferenceTraits"; import CatalogMemberTraits from "../Traits/TraitsClasses/CatalogMemberTraits"; import AccessControlMixin from "./AccessControlMixin"; import GroupMixin from "./GroupMixin"; import MappableMixin from "./MappableMixin"; import ReferenceMixin from "./ReferenceMixin"; type BaseType = Model<CatalogMemberTraits>; function CatalogMemberMixin<T extends AbstractConstructor<BaseType>>(Base: T) { abstract class CatalogMemberMixin extends AccessControlMixin(Base) implements SelectableDimensions, ViewingControls { abstract get type(): string; // The names of items in the CatalogMember's info array that contain details of the source of this CatalogMember's data. // This should be overridden by children of this class. For an example see the WebMapServiceCatalogItem _sourceInfoItemNames: string[] | undefined = undefined; constructor(...args: any[]) { super(...args); makeObservable(this); } get typeName(): string | undefined { return; } private _metadataLoader = new AsyncLoader( this.forceLoadMetadata.bind(this) ); get loadMetadataResult() { return this._metadataLoader.result; } /** * Gets a value indicating whether metadata is currently loading. */ get isLoadingMetadata(): boolean { return this._metadataLoader.isLoading; } @computed get isLoading() { return ( this.isLoadingMetadata || (MappableMixin.isMixedInto(this) && this.isLoadingMapItems) || (ReferenceMixin.isMixedInto(this) && this.isLoadingReference) || (GroupMixin.isMixedInto(this) && this.isLoadingMembers) ); } /** Calls AsyncLoader to load metadata. It is safe to call this as often as necessary. * If metadata is 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 loadMetadata(): Promise<Result<void>> { return (await this._metadataLoader.load()).clone({ message: `Failed to load \`${getName(this)}\` metadata`, importance: -1 }); } /** * Forces load of the metadata. This method does _not_ need to consider * whether the metadata is already loaded. * * You **can not** make changes to observables until **after** an asynchronous call {@see AsyncLoader}. * * Errors can be thrown here. * * {@see AsyncLoader} */ protected async forceLoadMetadata() {} get hasCatalogMemberMixin() { return true; } @computed get inWorkbench() { return this.terria.workbench.contains(this); } @override get name() { return super.name || this.uniqueId; } @override get nameInCatalog(): string | undefined { return super.nameInCatalog || this.name; } @computed get nameSortKey() { const parts = (this.nameInCatalog || "").split(/(\d+)/); return parts.map(function (part) { const parsed = parseInt(part, 10); if (parsed === parsed) { return parsed; } else { return part.trim().toLowerCase(); } }); } @computed get hasDescription(): boolean { return ( (isJsonString(this.description) && this.description.length > 0) || (isObservableArray(this.info) && this.info.some((info) => descriptionRegex.test(info.name || ""))) ); } @computed get infoAsObject() { const infoObject: any = {}; this.info.forEach((infoItem) => { if (infoItem.name !== undefined && infoItem.name.length > 0) { const infoNameNoSpaces = infoItem.name.replace(/ /g, ""); if ( isDefined(infoItem.content) && !isDefined(infoObject[infoNameNoSpaces]) ) { infoObject[infoNameNoSpaces] = infoItem.content; } else if (isDefined(infoItem.contentAsObject)) { infoObject[infoNameNoSpaces] = infoItem.contentAsObject; } } }); return infoObject; } @computed get infoWithoutSources() { const sourceInfoItemNames = this._sourceInfoItemNames; if (sourceInfoItemNames === undefined) { return this.info; } else { return this.info.filter((infoItem) => { if (infoItem.name === undefined) return true; return sourceInfoItemNames.indexOf(infoItem.name) === -1; }); } } /** Converts modelDimensions to selectableDimensions * This will apply modelDimension JSON value to user stratum */ @computed get selectableDimensions(): SelectableDimension[] { return ( this.modelDimensions.map((dim) => ({ id: dim.id, name: dim.name, selectedId: dim.selectedId, disable: dim.disable, allowUndefined: dim.allowUndefined, options: dim.options, setDimensionValue: (stratumId: string, selectedId: string) => { runInAction(() => dim.setTrait(stratumId, "selectedId", selectedId) ); const value = dim.options.find((o) => o.id === selectedId)?.value; if (isDefined(value)) { const result = updateModelFromJson( this, stratumId, mustacheNestedJsonObject(toJS(value), this) ); result.raiseError( this.terria, `Failed to update catalog item ${getName(this)}` ); // If no error then call loadMapItems if (!result.error && MappableMixin.isMixedInto(this)) { this.loadMapItems().then((loadMapItemsResult) => { loadMapItemsResult.raiseError(this.terria); }); } } } })) ?? [] ); } @computed get viewingControls(): ViewingControl[] { return []; } dispose() { super.dispose(); this._metadataLoader.dispose(); } } return CatalogMemberMixin; } const descriptionRegex = /description/i; namespace CatalogMemberMixin { export interface Instance extends InstanceType< ReturnType<typeof CatalogMemberMixin> > {} export function isMixedInto(model: any): model is Instance { return model && model.hasCatalogMemberMixin; } } export default CatalogMemberMixin; /** Convenience function to get user readable name of a BaseModel */ export const getName = action((model: BaseModel | undefined) => { return ( (CatalogMemberMixin.isMixedInto(model) ? model.name : undefined) ?? (hasTraits(model, CatalogMemberReferenceTraits, "name") ? model.name : undefined) ?? model?.uniqueId ?? "Unknown model" ); }); /** Recursively apply mustache template to all nested string properties in a JSON Object */ function mustacheNestedJsonObject(obj: JsonObject, view: any) { return Object.entries(obj).reduce<JsonObject>((acc, [key, value]) => { if (isJsonString(value)) { acc[key] = Mustache.render(value, view); } else if (isJsonObject(value, false)) { acc[key] = mustacheNestedJsonObject(value, view); } else { acc[key] = value; } return acc; }, {}); }