UNPKG

terriajs

Version:

Geospatial data visualization platform.

318 lines (279 loc) 10 kB
import { action, computed, observable, runInAction } from "mobx"; import RequestErrorEvent from "terriajs-cesium/Source/Core/RequestErrorEvent"; import Constructor from "../Core/Constructor"; import filterOutUndefined from "../Core/filterOutUndefined"; import isDefined from "../Core/isDefined"; import TerriaError from "../Core/TerriaError"; import MappableMixin, { MapItem } from "./MappableMixin"; import CommonStrata from "../Models/Definition/CommonStrata"; import createStratumInstance from "../Models/Definition/createStratumInstance"; import LoadableStratum from "../Models/Definition/LoadableStratum"; import Model, { BaseModel } from "../Models/Definition/Model"; import StratumOrder from "../Models/Definition/StratumOrder"; import CatalogFunctionJobTraits from "../Traits/TraitsClasses/CatalogFunctionJobTraits"; import { InfoSectionTraits } from "../Traits/TraitsClasses/CatalogMemberTraits"; import AutoRefreshingMixin from "./AutoRefreshingMixin"; import CatalogMemberMixin from "./CatalogMemberMixin"; import GroupMixin from "./GroupMixin"; class FunctionJobStratum extends LoadableStratum(CatalogFunctionJobTraits) { constructor(readonly catalogFunctionJob: CatalogFunctionJobMixin.Instance) { super(); } duplicateLoadableStratum(model: BaseModel): this { return new FunctionJobStratum( model as CatalogFunctionJobMixin.Instance ) as this; } @computed get shortReportSections() { if (this.catalogFunctionJob.logs.length === 0) return; return [ { name: "Job Logs", content: this.catalogFunctionJob.logs.join("\n"), show: true } ]; } @computed get shortReport() { let content = ""; if (this.catalogFunctionJob.jobStatus === "inactive") { content = "Job is inactive"; } else if (this.catalogFunctionJob.jobStatus === "running") { content = "Job is running..."; // If job is running, but not polling - then warn user to not leave the page if (!this.catalogFunctionJob.refreshEnabled) { content += "\n\nPlease do not leave this page &mdash; or results may be lost"; } } else if (this.catalogFunctionJob.jobStatus === "finished") { if (this.catalogFunctionJob.downloadedResults) { content = "Job is finished"; } else { content = "Job is finished, downloading results..."; } } else { content = "An error has occurred"; } return content; } @computed get description() { if (this.catalogFunctionJob.jobStatus === "finished") return `This is the result of invoking ${this.catalogFunctionJob.name} with the input parameters below.`; } @computed get info() { if ( isDefined(this.catalogFunctionJob.parameters) && Object.values(this.catalogFunctionJob.parameters).length > 0 ) { const inputsSection = '<table class="cesium-infoBox-defaultTable">' + Object.keys(this.catalogFunctionJob.parameters).reduce( (previousValue, key) => { return ( previousValue + "<tr>" + '<td style="vertical-align: middle">' + key + "</td>" + "<td>" + this.catalogFunctionJob.parameters![key] + "</td>" + "</tr>" ); }, "" ) + "</table>"; return [ createStratumInstance(InfoSectionTraits, { name: "Inputs", content: inputsSection }) ]; } } } type CatalogFunctionJobMixin = Model<CatalogFunctionJobTraits>; function CatalogFunctionJobMixin< T extends Constructor<CatalogFunctionJobMixin> >(Base: T) { abstract class CatalogFunctionJobMixin extends GroupMixin( AutoRefreshingMixin(CatalogMemberMixin(Base)) ) { constructor(...args: any[]) { super(...args); // Add FunctionJobStratum to strata runInAction(() => { this.strata.set(FunctionJobStratum.name, new FunctionJobStratum(this)); }); } /** * * @returns true for FINISHED, false for RUNNING (will then call pollForResults) */ protected abstract async _invoke(): Promise<boolean>; public async invoke() { this.setTrait(CommonStrata.user, "jobStatus", "running"); try { const finished = await runInAction(() => this._invoke()); if (finished) { this.setTrait(CommonStrata.user, "jobStatus", "finished"); await this.onJobFinish(true); } else { this.setTrait(CommonStrata.user, "refreshEnabled", true); } } catch (error) { this.setTrait(CommonStrata.user, "jobStatus", "error"); // Note: we set raiseToUser argument as false here, as it is handled in CatalogFunctionMixin.submitJob() this.setOnError(error, false); throw error; } } get refreshInterval() { return 2; } private pollingForResults = false; /** * Called every refreshInterval * * @return true if job has finished, false otherwise */ async pollForResults(): Promise<boolean> { throw "pollForResults not implemented"; } /** * This function adapts AutoRefreshMixin's refreshData with this Mixin's pollForResults - adding the boolean return value which triggers refresh disable */ @action refreshData() { if (this.pollingForResults) { return; } this.pollingForResults = true; (async () => { try { const finished = await this.pollForResults(); if (finished) { runInAction(() => { this.setTrait(CommonStrata.user, "jobStatus", "finished"); this.setTrait(CommonStrata.user, "refreshEnabled", false); }); await this.onJobFinish(true); } this.pollingForResults = false; } catch (error) { runInAction(() => { this.setTrait(CommonStrata.user, "jobStatus", "error"); this.setTrait(CommonStrata.user, "refreshEnabled", false); this.setOnError(error); }); this.pollingForResults = false; } })(); } private downloadingResults = false; /** * This handles downloading job results, it can be triggered three ways: * - `_invoke` returns true {@link CatalogFunctionJobMixin#invoke} * - `pollForResults` returns true {@link CatalogFunctionJobMixin#refreshData} * - on `loadMetadata` if `jobStatus` is "finished", and `!downloadedResults` {@link CatalogFunctionJobMixin#forceLoadMetadata} */ private async onJobFinish(addResultsToWorkbench = this.inWorkbench) { // Download results when finished if ( this.jobStatus === "finished" && !this.downloadedResults && !this.downloadingResults ) { this.downloadingResults = true; this.results = (await this.downloadResults()) || []; this.results.forEach((result) => { if (MappableMixin.isMixedInto(result)) result.setTrait(CommonStrata.user, "show", true); if (addResultsToWorkbench) this.terria.workbench .add(result) .then((r) => r.raiseError(this.terria)); this.terria.addModel(result); }); runInAction(() => { this.setTrait( CommonStrata.user, "members", filterOutUndefined(this.results.map((result) => result.uniqueId)) ); this.setTrait(CommonStrata.user, "downloadedResults", true); }); this.downloadingResults = false; } } /** * Job result CatalogMembers - set from calling {@link CatalogFunctionJobMixin#downloadResults} */ @observable public results: CatalogMemberMixin.Instance[] = []; /** * Called in {@link CatalogFunctionJobMixin#onJobFinish} * @returns catalog members to add to workbench */ abstract async downloadResults(): Promise< CatalogMemberMixin.Instance[] | void >; @action protected setOnError(error: unknown, raiseToUser: boolean = true) { const terriaError = TerriaError.from(error, { title: "Job failed", message: `An error has occurred while executing \`${this.name}\` job`, importance: -1 }); const errorMessage = terriaError.highestImportanceError.message; this.setTrait(CommonStrata.user, "logs", [...this.logs, errorMessage]); this.setTrait( CommonStrata.user, "shortReport", `${ this.typeName || this.type } invocation failed. More details are available on the Info panel.` ); const errorInfo = createStratumInstance(InfoSectionTraits, { name: `${this.typeName || this.type} invocation failed.`, content: errorMessage ?? "The reason for failure is unknown." }); const info = this.getTrait(CommonStrata.user, "info"); if (isDefined(info)) { info.push(errorInfo); } else { this.setTrait(CommonStrata.user, "info", [errorInfo]); } if (raiseToUser) this.terria.raiseErrorToUser(terriaError); } @computed get mapItems(): MapItem[] { return []; } protected async forceLoadMapItems() {} protected async forceLoadMetadata() { if (this.jobStatus === "finished" && !this.downloadedResults) { await this.onJobFinish(); } } protected async forceLoadMembers() {} get hasCatalogFunctionJobMixin() { return true; } } return CatalogFunctionJobMixin; } namespace CatalogFunctionJobMixin { StratumOrder.addLoadStratum(FunctionJobStratum.name); export interface Instance extends InstanceType<ReturnType<typeof CatalogFunctionJobMixin>> {} export function isMixedInto(model: any): model is Instance { return model && model.hasCatalogFunctionJobMixin; } } export default CatalogFunctionJobMixin;