"use strict";
/*global require*/
var i18next = require("i18next").default;
var ResultPendingCatalogItem = require("./ResultPendingCatalogItem");
var LineParameter = require("./LineParameter");
var RectangleParameter = require("./RectangleParameter");
var PolygonParameter = require("./PolygonParameter");
var CatalogFunction = require("./CatalogFunction");
var CesiumMath = require("terriajs-cesium/Source/Core/Math").default;
var DateTimeParameter = require("./DateTimeParameter");
var defined = require("terriajs-cesium/Source/Core/defined").default;
var EnumerationParameter = require("./EnumerationParameter");
var StringParameter = require("./StringParameter");
var inherit = require("../Core/inherit");
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var loadXML = require("../Core/loadXML");
var loadWithXhr = require("../Core/loadWithXhr");
var PointParameter = require("./PointParameter");
var proxyCatalogItemUrl = require("./proxyCatalogItemUrl");
var runLater = require("../Core/runLater");
var sprintf = require("terriajs-cesium/Source/ThirdParty/sprintf").default;
var TerriaError = require("../Core/TerriaError");
var URI = require("urijs");
var WebProcessingServiceCatalogItem = require("./WebProcessingServiceCatalogItem");
var Reproject = require("../Map/Reproject");
var when = require("terriajs-cesium/Source/ThirdParty/when").default;
var xml2json = require("../ThirdParty/xml2json");
var Mustache = require("mustache");
var executeWpsTemplate = require("./ExecuteWpsTemplate.xml");
var GeoJsonParameter = require("./GeoJsonParameter");
var RegionTypeParameter = require("./RegionTypeParameter");
var RegionParameter = require("./RegionParameter");
/**
* A {@link CatalogFunction} that invokes a Web Processing Service (WPS) process.
*
* @alias WebProcessingServiceCatalogFunction
* @constructor
* @extends CatalogFunction
*
* @param {Terria} terria The Terria instance.
*/
function WebProcessingServiceCatalogFunction(terria) {
CatalogFunction.call(this, terria);
this._statusSupported = false;
this._storeSupported = false;
/**
* Gets or sets the URL of the WPS server. This property is observable.
* @type {String}
*/
this.url = undefined;
/**
* Gets or sets the identifier of this WPS process. This property is observable.
* @type {String}
*/
this.identifier = undefined;
/**
* Gets or sets whether to use key value pairs (KVP) embedded in Execute URL, or whether to make a POST request
* with XML data.
* @type {Boolean}
*/
this.executeWithHttpGet = undefined;
knockout.track(this, [
"_parameters",
"url",
"identifier",
"inputs",
"executeWithHttpGet"
]);
}
inherit(CatalogFunction, WebProcessingServiceCatalogFunction);
Object.defineProperties(WebProcessingServiceCatalogFunction.prototype, {
/**
* Gets the type of data item represented by this instance.
* @memberOf WebProcessingServiceCatalogFunction.prototype
* @type {String}
*/
type: {
get: function() {
return "wps";
}
},
/**
* Gets a human-readable name for this type of data source, 'Web Processing Service (WPS)'.
* @memberOf WebProcessingServiceCatalogFunction.prototype
* @type {String}
*/
typeName: {
get: function() {
return i18next.t("models.webProcessingService.wps");
}
},
/**
* Gets the parameters used to {@link CatalogFunction#invoke} to this process.
* @memberOf WebProcessingServiceCatalogFunction
* @type {CatalogFunctionParameters[]}
*/
parameters: {
get: function() {
return this._parameters;
}
}
});
/**
* Gets or sets the list of converters between a WPS Input and a {@link FunctionParameter}.
* @type {Array}
*/
WebProcessingServiceCatalogFunction.parameterConverters = [
{
id: "LiteralData",
inputToFunctionParameter: function(catalogFunction, input) {
if (!defined(input.LiteralData)) {
return undefined;
}
var allowedValues = input.LiteralData.AllowedValues;
if (defined(input.LiteralData.AllowedValue)) {
// OGC 05-007r7 Table 29 specifies AllowedValues as name of values for input, not AllowedValue, but for
// backward compatibility, allow AllowedValue.
allowedValues = input.LiteralData.AllowedValue;
}
if (defined(allowedValues)) {
var allowed = allowedValues.Value.slice();
if (typeof allowed === "string") {
allowed = [allowed];
}
return new EnumerationParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
possibleValues: allowed,
isRequired: (input.minOccurs | 0) > 0
});
} else if (defined(input.LiteralData.AnyValue)) {
return new StringParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0
});
} else {
return undefined;
}
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return {
inputValue: value,
inputType: "LiteralData"
};
}
},
{
id: "DateTime",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.ComplexData) ||
!defined(input.ComplexData.Default) ||
!defined(input.ComplexData.Default.Format) ||
!defined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
var schema = input.ComplexData.Default.Format.Schema;
if (schema !== "http://www.w3.org/TR/xmlschema-2/#dateTime") {
return undefined;
}
return new DateTimeParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return {
inputType: "ComplexData",
inputValue: DateTimeParameter.formatValueForUrl(value)
};
}
},
{
id: "PointGeometry",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.ComplexData) ||
!defined(input.ComplexData.Default) ||
!defined(input.ComplexData.Default.Format) ||
!defined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
var schema = input.ComplexData.Default.Format.Schema;
if (schema.indexOf("http://geojson.org/geojson-spec.html#") !== 0) {
return undefined;
}
if (schema.substring(schema.lastIndexOf("#") + 1) !== "point") {
return undefined;
}
return new PointParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return {
inputType: "ComplexData",
inputValue: PointParameter.formatValueForUrl(value)
};
}
},
{
id: "LineGeometry",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.ComplexData) ||
!defined(input.ComplexData.Default) ||
!defined(input.ComplexData.Default.Format) ||
!defined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
var schema = input.ComplexData.Default.Format.Schema;
if (schema.indexOf("http://geojson.org/geojson-spec.html#") !== 0) {
return undefined;
}
if (schema.substring(schema.lastIndexOf("#") + 1) !== "linestring") {
return undefined;
}
return new LineParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return {
inputType: "ComplexData",
inputValue: LineParameter.formatValueForUrl(value)
};
}
},
{
id: "PolygonGeometry",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.ComplexData) ||
!defined(input.ComplexData.Default) ||
!defined(input.ComplexData.Default.Format) ||
!defined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
var schema = input.ComplexData.Default.Format.Schema;
if (schema.indexOf("http://geojson.org/geojson-spec.html#") !== 0) {
return undefined;
}
if (schema.substring(schema.lastIndexOf("#") + 1) !== "polygon") {
return undefined;
}
return new PolygonParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return {
inputType: "ComplexData",
inputValue: PolygonParameter.formatValueForUrl(value)
};
}
},
{
id: "RectangleGeometry",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.BoundingBoxData) ||
!defined(input.BoundingBoxData.Default) ||
!defined(input.BoundingBoxData.Default.CRS)
) {
return undefined;
}
var code = Reproject.crsStringToCode(input.BoundingBoxData.Default.CRS);
var usedCrs = input.BoundingBoxData.Default.CRS;
// Find out if Terria's CRS is supported.
if (code !== Reproject.TERRIA_CRS) {
for (var i = 0; i < input.BoundingBoxData.Supported.CRS.length; i++) {
if (
Reproject.crsStringToCode(
input.BoundingBoxData.Supported.CRS[i]
) === Reproject.TERRIA_CRS
) {
code = Reproject.TERRIA_CRS;
usedCrs = input.BoundingBoxData.Supported.CRS[i];
break;
}
}
}
// We are currently only supporting Terria's CRS, because if we reproject we don't know the URI or whether
// the bounding box order is lat-long or long-lat.
if (!defined(code)) {
return undefined;
}
return new RectangleParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0,
crs: usedCrs
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
var bboxMinCoord1, bboxMinCoord2, bboxMaxCoord1, bboxMaxCoord2, urn;
// We only support CRS84 and EPSG:4326
if (parameter.crs.indexOf("crs84") !== -1) {
// CRS84 uses long, lat rather that lat, long order.
bboxMinCoord1 = CesiumMath.toDegrees(value.west);
bboxMinCoord2 = CesiumMath.toDegrees(value.south);
bboxMaxCoord1 = CesiumMath.toDegrees(value.east);
bboxMaxCoord2 = CesiumMath.toDegrees(value.north);
// Comfortingly known as WGS 84 longitude-latitude according to Table 3 in OGC 07-092r1.
urn = "urn:ogc:def:crs:OGC:1.3:CRS84";
} else {
// The URN value urn:ogc:def:crs:EPSG:6.6:4326 shall mean the Coordinate Reference System (CRS) with code
// 4326 specified in version 6.6 of the EPSG database available at http://www.epsg.org/. That CRS specifies
// the axis order as Latitude followed by Longitude.
// We don't know about other URN versions, so are going to return 6.6 regardless of what was requested.
bboxMinCoord1 = CesiumMath.toDegrees(value.south);
bboxMinCoord2 = CesiumMath.toDegrees(value.west);
bboxMaxCoord1 = CesiumMath.toDegrees(value.north);
bboxMaxCoord2 = CesiumMath.toDegrees(value.east);
urn = "urn:ogc:def:crs:EPSG:6.6:4326";
}
return {
inputType: "BoundingBoxData",
inputValue:
bboxMinCoord1 +
"," +
bboxMinCoord2 +
"," +
bboxMaxCoord1 +
"," +
bboxMaxCoord2 +
"," +
urn
};
}
},
{
id: "GeoJsonGeometry",
inputToFunctionParameter: function(catalogFunction, input) {
if (
!defined(input.ComplexData) ||
!defined(input.ComplexData.Default) ||
!defined(input.ComplexData.Default.Format) ||
!defined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
var schema = input.ComplexData.Default.Format.Schema;
if (
schema.indexOf("http://geojson.org/geojson-spec.html#") === 0 &&
schema.indexOf("http://geojson.org/geojson-spec.html") !== 0
) {
return undefined;
}
var regionTypeParameter = new RegionTypeParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: "regionType",
name: i18next.t("models.webProcessingService.regionTypeName"),
description: i18next.t(
"models.webProcessingService.regionTypeDescription"
)
});
var regionParameter = new RegionParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: "regionParameter",
name: i18next.t("models.webProcessingService.regionParameterName"),
regionProvider: regionTypeParameter
});
return new GeoJsonParameter({
terria: catalogFunction.terria,
catalogFunction: catalogFunction,
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired: (input.minOccurs | 0) > 0,
regionParameter: regionParameter
});
},
functionParameterToInput: function(catalogFunction, parameter, value) {
return parameter.getProcessedValue(value);
}
}
];
WebProcessingServiceCatalogFunction.prototype._load = function() {
var uri = new URI(this.url).query({
service: "WPS",
request: "DescribeProcess",
version: "1.0.0",
Identifier: this.identifier
});
var url = proxyCatalogItemUrl(this, uri.toString(), "1d");
var that = this;
return loadXML(url).then(function(xml) {
// Is this really a DescribeProcess response?
if (
!xml ||
!xml.documentElement ||
xml.documentElement.localName !== "ProcessDescriptions"
) {
throw new TerriaError({
title: i18next.t("models.webProcessingService.invalidWPSServerTitle"),
message: i18next.t(
"models.webProcessingService.invalidWPSServerMessage",
{
name: that.name,
email:
'<a href="mailto:' +
that.terria.supportEmail +
'">' +
that.terria.supportEmail +
"</a>."
}
)
});
}
var json = xml2json(xml);
if (!defined(json.ProcessDescription)) {
throw new TerriaError({
sender: that,
title: i18next.t(
"models.webProcessingService.processDescriptionErrorTitle"
),
message: i18next.t(
"models.webProcessingService.processDescriptionErrorMessage"
)
});
}
that._storeSupported = json.ProcessDescription.storeSupported === "true";
that._statusSupported = json.ProcessDescription.statusSupported === "true";
function throwNoInputs() {
throw new TerriaError({
sender: that,
title: i18next.t("models.webProcessingService.processInputErrorTitle"),
message: i18next.t(
"models.webProcessingService.processInputErrorMessage"
)
});
}
var dataInputs = json.ProcessDescription.DataInputs;
if (!defined(dataInputs)) {
throwNoInputs();
}
var inputs = dataInputs.Input;
if (!defined(inputs)) {
throwNoInputs();
}
if (!Array.isArray(inputs)) {
inputs = [inputs];
}
that._parameters = inputs.map(
createParameterFromWpsInput.bind(undefined, that)
);
});
};
/**
* Invoke the WPS function with the provided parameterValues.
* @return {Promise}
*/
WebProcessingServiceCatalogFunction.prototype.invoke = function() {
var now = new Date();
var timestamp = sprintf(
"%04d-%02d-%02dT%02d:%02d:%02d",
now.getFullYear(),
now.getMonth() + 1,
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds()
);
var asyncResult = new ResultPendingCatalogItem(this.terria);
asyncResult.name = this.name + " " + timestamp;
asyncResult.description = i18next.t(
"models.webProcessingService.resultPendingDescription",
{ name: this.name, timestamp: timestamp }
);
var inputsSection =
'<table class="cesium-infoBox-defaultTable">' +
(this.parameters || []).reduce(function(previousValue, parameter) {
return (
previousValue +
"<tr>" +
'<td style="vertical-align: middle">' +
parameter.name +
"</td>" +
"<td>" +
parameter.formatValueAsString(parameter.value) +
"</td>" +
"</tr>"
);
}, "") +
"</table>";
asyncResult.info.push({
name: "Inputs",
content: inputsSection
});
var that = this;
when.all(createWpsDataInputsFromParameters(this)).then(function(dataInputs) {
var proxyCacheDuration = "1d";
var promise;
if (that.executeWithHttpGet) {
promise = loadResponseWithKvp(
that,
dataInputs,
asyncResult,
proxyCacheDuration
);
} else {
promise = loadResponse(that, dataInputs, asyncResult, proxyCacheDuration);
}
asyncResult.loadPromise = promise;
asyncResult.isEnabled = true;
return promise;
});
};
function loadResponseWithKvp(
that,
dataInputs,
asyncResult,
proxyCacheDuration
) {
dataInputs = dataInputs.join(";");
var uri = new URI(that.url).query({
service: "WPS",
request: "Execute",
version: "1.0.0",
Identifier: that.identifier,
DataInputs: dataInputs
});
if (that._statusSupported) {
uri.addQuery("status", true);
}
if (that._storeSupported) {
uri.addQuery("storeExecuteResponse", true);
}
var url = proxyCatalogItemUrl(that, uri.toString(), proxyCacheDuration);
var parameterValues = that.getParameterValues();
var promise = loadXML(url).then(function(xml) {
return handleExecuteResponse(that, parameterValues, asyncResult, xml);
});
return promise;
}
function loadResponse(that, dataInputs, asyncResult, proxyCacheDuration) {
var parameters = {
identifier: htmlEscapeText(that.identifier),
storeExecuteResponse: that._storeSupported,
status: that._statusSupported,
dataInputs: dataInputs
};
var xmlInput = Mustache.render(executeWpsTemplate, parameters);
var uri = new URI(that.url).query({
service: "WPS",
request: "Execute"
});
var url = proxyCatalogItemUrl(that, uri.toString(), proxyCacheDuration);
var parameterValues = that.getParameterValues();
var promise = loadWithXhr({
url: url,
method: "POST",
data: xmlInput,
overrideMimeType: "text/xml",
responseType: "document"
}).then(function(xml) {
return handleExecuteResponse(that, parameterValues, asyncResult, xml);
});
return promise;
}
function createParameterFromWpsInput(catalogFunction, input) {
for (
var i = 0;
i < WebProcessingServiceCatalogFunction.parameterConverters.length;
++i
) {
var converter = WebProcessingServiceCatalogFunction.parameterConverters[i];
var functionParameter = converter.inputToFunctionParameter(
catalogFunction,
input
);
if (defined(functionParameter)) {
functionParameter.converter = converter;
return functionParameter;
}
}
throw new TerriaError({
sender: catalogFunction,
title: i18next.t(
"models.webProcessingService.unsupportedParameterErrorTitle"
),
message: i18next.t(
"models.webProcessingService.unsupportedParameterErrorMessage",
{ identifier: input.Identifier }
)
});
}
function htmlEscapeText(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function createWpsDataInputsFromParameters(catalogFunction) {
return catalogFunction.parameters
.map(function(parameter) {
var value = parameter.value;
if (!defined(value) || value === "") {
return undefined;
}
var processedValue = parameter.converter.functionParameterToInput(
catalogFunction,
parameter,
value
);
return when(processedValue.inputValue).then(function(inputValue) {
if (!defined(inputValue) || inputValue === "") {
return undefined;
}
if (catalogFunction.executeWithHttpGet) {
return parameter.id + "=" + inputValue;
} else {
var dataInputObj = {
inputIdentifier: htmlEscapeText(parameter.id),
inputValue: htmlEscapeText(inputValue),
inputType: htmlEscapeText(processedValue.inputType)
};
return dataInputObj;
}
});
})
.filter(function(convertedParameter) {
return defined(convertedParameter);
});
}
function handleExecuteResponse(
catalogFunction,
parameterValues,
asyncResult,
xmlResponse
) {
if (
!xmlResponse ||
!xmlResponse.documentElement ||
xmlResponse.documentElement.localName !== "ExecuteResponse"
) {
throw new TerriaError({
sender: catalogFunction,
title: i18next.t("models.webProcessingService.invalidResponseErrorTitle"),
message: i18next.t(
"models.webProcessingService.invalidResponseErrorMessage",
{
name: catalogFunction.name,
email:
'<a href="mailto:' +
catalogFunction.terria.supportEmail +
'">' +
catalogFunction.terria.supportEmail +
"</a>."
}
)
});
}
var json = xml2json(xmlResponse);
var status = json.Status;
if (!defined(status)) {
throw new TerriaError({
sender: catalogFunction,
title: i18next.t(
"models.webProcessingService.invalidResponseError2Title"
),
message: i18next.t(
"models.webProcessingService.invalidResponseError2Message"
)
});
}
if (defined(status.ProcessFailed)) {
var errorMessage = i18next.t(
"models.webProcessingService.failureReasonUnknowError"
);
if (
defined(status.ProcessFailed.ExceptionReport) &&
defined(status.ProcessFailed.ExceptionReport.Exception)
) {
if (
defined(status.ProcessFailed.ExceptionReport.Exception.ExceptionText)
) {
errorMessage =
status.ProcessFailed.ExceptionReport.Exception.ExceptionText;
} else if (
defined(status.ProcessFailed.ExceptionReport.Exception.Exception)
) {
errorMessage = status.ProcessFailed.ExceptionReport.Exception.Exception;
}
}
asyncResult.isFailed = true;
asyncResult.shortReport = i18next.t(
"models.webProcessingService.asyncShortReportFailed"
);
asyncResult.moreFailureDetailsAvailable = true;
asyncResult.info.push({
name: i18next.t("models.webProcessingService.errorDetails"),
content: errorMessage
});
} else if (defined(status.ProcessSucceeded)) {
var resultCatalogItem = new WebProcessingServiceCatalogItem(
catalogFunction.terria
);
resultCatalogItem.name = asyncResult.name;
resultCatalogItem.description = asyncResult.description;
resultCatalogItem.parameters = catalogFunction.parameters;
resultCatalogItem.parameterValues = parameterValues;
resultCatalogItem.wpsResponseUrl = json.statusLocation;
resultCatalogItem.wpsResponse = json;
resultCatalogItem.dataUrl = json.statusLocation;
// make this clickable if an absolute url path is returned
if (json.statusLocation.indexOf("/") === 0) {
resultCatalogItem.dataUrl =
catalogFunction.url.substr(0, catalogFunction.url.lastIndexOf("/")) +
json.statusLocation;
}
asyncResult.isEnabled = false;
resultCatalogItem.isEnabled = true;
} else if (defined(json.statusLocation) && asyncResult.isEnabled) {
// Continue polling the status location, waiting 500ms between each response and the next request.
return runLater(function() {
return loadXML(
proxyCatalogItemUrl(catalogFunction, json.statusLocation, "1d")
).then(function(xml) {
return handleExecuteResponse(
catalogFunction,
parameterValues,
asyncResult,
xml
);
});
}, 500);
}
}
module.exports = WebProcessingServiceCatalogFunction;