UNPKG

terriajs

Version:

Geospatial data visualization platform.

525 lines (466 loc) 15.4 kB
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; } @action static async load(item: WebProcessingServiceCatalogFunctionJob) { return new WpsLoadableStratum(item); } @computed 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; } @computed 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 }); } @computed 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; } @computed 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"; @observable public geoJsonItem?: GeoJsonCatalogItem; private get executeUrlParameters() { return { service: "WPS", request: "Execute", version: "1.0.0" }; } /** * Returns the proxied URL for the Execute endpoint. */ @computed 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 */ @action 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; } @computed 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 */ @computed 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, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;"); }