terriajs
Version:
Geospatial data visualization platform.
318 lines (279 loc) • 10 kB
text/typescript
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;
}
get shortReportSections() {
if (this.catalogFunctionJob.logs.length === 0) return;
return [
{
name: "Job Logs",
content: this.catalogFunctionJob.logs.join("\n"),
show: true
}
];
}
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 — 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;
}
get description() {
if (this.catalogFunctionJob.jobStatus === "finished")
return `This is the result of invoking ${this.catalogFunctionJob.name} with the input parameters below.`;
}
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
*/
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}
*/
public results: CatalogMemberMixin.Instance[] = [];
/**
* Called in {@link CatalogFunctionJobMixin#onJobFinish}
* @returns catalog members to add to workbench
*/
abstract async downloadResults(): Promise<
CatalogMemberMixin.Instance[] | void
>;
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);
}
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;