terriajs
Version:
Geospatial data visualization platform.
525 lines (466 loc) • 15.4 kB
text/typescript
import i18next from "i18next";
import {
action,
computed,
isObservableArray,
observable,
runInAction,
toJS
} from "mobx";
import Mustache from "mustache";
import URI from "urijs";
import isDefined from "../../../Core/isDefined";
import { JsonObject, isJsonObject } from "../../../Core/Json";
import TerriaError from "../../../Core/TerriaError";
import CatalogFunctionJobMixin from "../../../ModelMixins/CatalogFunctionJobMixin";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import XmlRequestMixin from "../../../ModelMixins/XmlRequestMixin";
import xml2json from "../../../ThirdParty/xml2json";
import { ShortReportTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits";
import { FeatureInfoTemplateTraits } from "../../../Traits/TraitsClasses/FeatureInfoTraits";
import WebProcessingServiceCatalogFunctionJobTraits from "../../../Traits/TraitsClasses/WebProcessingServiceCatalogFunctionJobTraits";
import CommonStrata from "../../Definition/CommonStrata";
import CreateModel from "../../Definition/CreateModel";
import createStratumInstance from "../../Definition/createStratumInstance";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import StratumOrder from "../../Definition/StratumOrder";
import updateModelFromJson from "../../Definition/updateModelFromJson";
import upsertModelFromJson from "../../Definition/upsertModelFromJson";
import GeoJsonCatalogItem from "../CatalogItems/GeoJsonCatalogItem";
import CatalogMemberFactory from "../CatalogMemberFactory";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
const executeWpsTemplate = require("./ExecuteWpsTemplate.xml");
const createGuid = require("terriajs-cesium/Source/Core/createGuid").default;
class WpsLoadableStratum extends LoadableStratum(
WebProcessingServiceCatalogFunctionJobTraits
) {
static stratumName = "wpsLoadable";
constructor(readonly item: WebProcessingServiceCatalogFunctionJob) {
super();
}
duplicateLoadableStratum(newModel: BaseModel): this {
return new WpsLoadableStratum(
newModel as WebProcessingServiceCatalogFunctionJob
) as this;
}
static async load(item: WebProcessingServiceCatalogFunctionJob) {
return new WpsLoadableStratum(item);
}
get shortReportSections() {
const reports = this.item.outputs
.map((output) => {
let report;
if (isDefined(output.Data.LiteralData)) {
report = createStratumInstance(ShortReportTraits, {
name: output.Title,
content: formatOutputValue(output.Title, output.Data.LiteralData)
});
}
return report;
})
.filter(isDefined);
return reports;
}
get featureInfoTemplate() {
const template = [
"#### Inputs\n\n" +
this.item.info.find((info) => info.name === "Inputs")?.content,
"#### Outputs\n\n" + this.outputsSectionHtml
].join("\n\n");
return createStratumInstance(FeatureInfoTemplateTraits, {
template
});
}
get outputsSectionHtml() {
const outputsSection =
'<table class="cesium-infoBox-defaultTable">' +
this.item.outputs.reduce((previousValue, output) => {
if (
!isDefined(output.Data) ||
(!isDefined(output.Data.LiteralData) &&
!isDefined(output.Data.ComplexData))
) {
return previousValue;
}
let content = "";
if (isDefined(output.Data.LiteralData)) {
content = formatOutputValue(output.Title, output.Data.LiteralData);
} else if (isDefined(output.Data.ComplexData)) {
if (output.Data.ComplexData.mimeType === "text/csv") {
content =
'<chart can-download="true" hide-buttons="false" title="' +
output.Title +
"\" data='" +
output.Data.ComplexData +
'\' styling: "feature-info">';
} else if (
output.Data.ComplexData.mimeType ===
"application/vnd.terriajs.catalog-member+json"
) {
content = "Chart " + output.Title + " generated.";
}
// Support other types of ComplexData here as it becomes necessary.
}
return (
previousValue +
"<tr>" +
'<td style="vertical-align: middle">' +
output.Title +
"</td>" +
"<td>" +
content +
"</td>" +
"</tr>"
);
}, "") +
"</table>";
return outputsSection;
}
get rectangle() {
return this.item.geoJsonItem?.rectangle;
}
}
StratumOrder.addLoadStratum(WpsLoadableStratum.stratumName);
export default class WebProcessingServiceCatalogFunctionJob extends XmlRequestMixin(
CatalogFunctionJobMixin(
CreateModel(WebProcessingServiceCatalogFunctionJobTraits)
)
) {
static readonly type = "wps-result";
get type() {
return WebProcessingServiceCatalogFunctionJob.type;
}
get typeName() {
return i18next.t("models.webProcessingService.wpsResult");
}
readonly proxyCacheDuration = "1d";
public geoJsonItem?: GeoJsonCatalogItem;
private get executeUrlParameters() {
return {
service: "WPS",
request: "Execute",
version: "1.0.0"
};
}
/**
* Returns the proxied URL for the Execute endpoint.
*/
get executeUrl() {
if (!isDefined(this.url)) {
return;
}
const uri = new URI(this.url).query(this.executeUrlParameters);
return proxyCatalogItemUrl(this, uri.toString(), this.proxyCacheDuration);
}
async _invoke() {
if (
!isDefined(this.identifier) ||
!isDefined(this.executeUrl) ||
!isDefined(this.wpsParameters)
) {
throw `Identifier, executeUrl and wpsParameters must be set`;
}
const identifier = this.identifier;
const executeUrl = this.executeUrl;
const parameters = {
...this.executeUrlParameters,
Identifier: htmlEscapeText(identifier),
DataInputs: toJS(this.wpsParameters),
storeExecuteResponse: toJS(this.storeSupported),
status: toJS(this.statusSupported)
};
let promise: Promise<any>;
if (this.executeWithHttpGet) {
promise = this.getXml(executeUrl, {
...parameters,
DataInputs: parameters.DataInputs.map(
({ inputIdentifier: id, inputValue: val }) => `${id}=${val}`
).join(";")
});
} else {
const executeXml = Mustache.render(executeWpsTemplate, parameters);
promise = this.postXml(executeUrl, executeXml);
}
const executeResponseXml = await promise;
if (
!executeResponseXml ||
!executeResponseXml.documentElement ||
executeResponseXml.documentElement.localName !== "ExecuteResponse"
) {
throw `Invalid XML response for WPS ExecuteResponse`;
}
const json = xml2json(executeResponseXml);
// Check if finished
if (this.checkStatus(json)) {
// set result
runInAction(() => this.setTrait(CommonStrata.user, "wpsResponse", json));
return true;
}
// If not finished, set response URL for polling
runInAction(() =>
this.setTrait(CommonStrata.user, "wpsResponseUrl", json.statusLocation)
);
return false;
}
/**
* Return true for finished, false for running, throw error otherwise
*/
checkStatus(json: any) {
const status = json.Status;
if (!isDefined(status)) {
throw new TerriaError({
sender: this,
title: i18next.t(
"models.webProcessingService.invalidResponseErrorTitle"
),
message: i18next.t(
"models.webProcessingService.invalidResponseErrorMessage",
{
name: this.name
}
)
});
}
if (isDefined(status.ProcessFailed)) {
throw (
status.ProcessFailed.ExceptionReport?.Exception?.ExceptionText ||
JSON.stringify(status.ProcessFailed)
);
} else if (isDefined(status.ProcessSucceeded)) {
return true;
}
return false;
}
async pollForResults() {
if (!isDefined(this.wpsResponseUrl)) {
return true;
}
const promise = this.getXml(
proxyCatalogItemUrl(this, this.wpsResponseUrl, "0d")
);
const xml = await promise;
const json = xml2json(xml);
return this.checkStatus(json);
}
async downloadResults() {
if (isDefined(this.wpsResponseUrl) && !isDefined(this.wpsResponse)) {
const url = proxyCatalogItemUrl(this, this.wpsResponseUrl, "0d");
const wpsResponse = xml2json(await this.getXml(url));
runInAction(() => {
this.setTrait(CommonStrata.user, "wpsResponse", wpsResponse);
});
}
if (!isDefined(this.wpsResponse)) return;
const reports: StratumFromTraits<ShortReportTraits>[] = [];
const outputs = runInAction(() => this.outputs);
const results: CatalogMemberMixin.Instance[] = [];
await Promise.all(
outputs.map(async (output, i) => {
if (!output.Data.ComplexData) {
return;
}
let reportContent = output.Data.ComplexData;
if (output.Data.ComplexData.mimeType === "text/csv") {
reportContent =
'<collapsible title="' +
output.Title +
'" open="' +
(i === 0 ? "true" : "false") +
'">';
reportContent +=
'<chart can-download="true" hide-buttons="false" title="' +
output.Title +
"\" data='" +
output.Data.ComplexData.text +
'\' styling="histogram"></chart>';
reportContent += "</collapsible>";
} else if (
output.Data.ComplexData.mimeType ===
"application/vnd.terriajs.catalog-member+json"
) {
// Create a catalog member from the embedded json
const json = JSON.parse(output.Data.ComplexData.text);
const catalogItem = await this.createCatalogItemFromJson(json);
if (isDefined(catalogItem)) {
if (CatalogMemberMixin.isMixedInto(catalogItem))
results.push(catalogItem);
reportContent = "Chart " + output.Title + " generated.";
}
}
reports.push(
createStratumInstance(ShortReportTraits, {
name: output.Title,
content: reportContent
})
);
})
);
// Create geojson catalog item for input features
const geojsonFeatures = runInAction(() => this.geojsonFeatures);
if (isJsonObject(geojsonFeatures, false)) {
runInAction(() => {
this.geoJsonItem = new GeoJsonCatalogItem(createGuid(), this.terria);
updateModelFromJson(this.geoJsonItem, CommonStrata.user, {
name: `${this.name} Input Features`,
// Use cesium primitives so we don't have to deal with feature picking/selection
forceCesiumPrimitives: true,
geoJsonData: {
type: "FeatureCollection",
features: geojsonFeatures,
totalFeatures: this.geojsonFeatures!.length
}
}).logError(
"Error ocurred while updating Input Features GeoJSON model JSON"
);
});
(await this.geoJsonItem!.loadMapItems()).throwIfError;
}
runInAction(() => {
this.setTrait(CommonStrata.user, "shortReportSections", reports);
});
return results;
}
get mapItems() {
if (isDefined(this.geoJsonItem)) {
return this.geoJsonItem.mapItems.map((mapItem) => {
mapItem.show = this.show;
return mapItem;
});
}
return [];
}
protected async forceLoadMetadata() {
await super.forceLoadMetadata();
const stratum = await WpsLoadableStratum.load(this);
runInAction(() => {
this.strata.set(WpsLoadableStratum.stratumName, stratum);
});
}
protected async forceLoadMapItems(): Promise<void> {
if (isDefined(this.geoJsonItem)) {
const geoJsonItem = this.geoJsonItem;
(await geoJsonItem.loadMapItems()).throwIfError();
}
}
/**
* Returns all the process outputs skipping process contexts and empty outputs
*/
get outputs() {
const wpsResponse = <any>this.wpsResponse;
if (
!wpsResponse ||
!wpsResponse.ProcessOutputs ||
!wpsResponse.ProcessOutputs.Output
) {
return [];
}
const obj = wpsResponse.ProcessOutputs.Output;
const outputs = Array.isArray(obj) || isObservableArray(obj) ? obj : [obj];
return outputs.filter(
(o) => o.Identifier !== ".context" && isDefined(o.Data)
);
}
private async createCatalogItemFromJson(json: any) {
let itemJson = json;
try {
if (
this.forceConvertResultsToV8 ||
// If startData.version has version 0.x.x - user catalog-converter to convert result
("version" in itemJson &&
typeof itemJson.version === "string" &&
itemJson.version.startsWith("0"))
) {
itemJson = await convertResultV7toV8(json);
}
} catch (e) {
throw e;
}
const catalogItem = upsertModelFromJson(
CatalogMemberFactory,
this.terria,
this.uniqueId || "",
CommonStrata.user,
{
...itemJson,
id: createGuid()
},
{
addModelToTerria: false
}
).throwIfError({
title: "WPS output error",
message: `Failed to create Terria model from WPS output${
itemJson.name ? `: ${itemJson.name}` : ""
}`
});
return catalogItem;
}
}
function formatOutputValue(title: string, value: string | undefined) {
if (!isDefined(value)) {
return "";
}
const values = value.split(",");
return values.reduce(function (previousValue, currentValue) {
if (value.match(/[.\/](png|jpg|jpeg|gif|svg)/i)) {
return (
previousValue +
'<a href="' +
currentValue +
'"><img src="' +
currentValue +
'" alt="' +
title +
'" /></a>'
);
} else if (
currentValue.indexOf("http:") === 0 ||
currentValue.indexOf("https:") === 0
) {
const uri = new URI(currentValue);
return (
previousValue +
'<a href="' +
currentValue +
'">' +
uri.filename() +
"</a>"
);
} else {
return previousValue + currentValue;
}
}, "");
}
async function convertResultV7toV8(
input: JsonObject
): Promise<Record<string, unknown> | undefined> {
const { convertMember, convertCatalog } = await import("catalog-converter");
if (typeof input.type === "string") {
const { member, messages } = convertMember(input);
if (member === null)
throw TerriaError.combine(
messages.map((m) => TerriaError.from(m.message)),
"Error converting v7 item to v8"
);
return member;
} else {
const { result, messages } = convertCatalog(input);
if (result === null)
throw TerriaError.combine(
messages.map((m) => TerriaError.from(m.message)),
"Error converting v7 catalog to v8"
);
return result;
}
}
function htmlEscapeText(text: string) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}