UNPKG

terriajs

Version:

Geospatial data visualization platform.

620 lines (558 loc) 25.7 kB
'use strict'; /*global require*/ 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'); var DateTimeParameter = require('./DateTimeParameter'); var defined = require('terriajs-cesium/Source/Core/defined'); var defineProperties = require('terriajs-cesium/Source/Core/defineProperties'); var EnumerationParameter = require('./EnumerationParameter'); var StringParameter = require('./StringParameter'); var inherit = require('../Core/inherit'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); 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'); 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'); var xml2json = require('../ThirdParty/xml2json'); var Mustache = require('mustache'); var executeWpsTemplate = require('./ExecuteWpsTemplate.xml'); /** * 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); 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 'Web Processing Service (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 }; } } ]; 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: 'Invalid WPS server', message: '\ An error occurred while invoking DescribeProcess on the WPS server for process name '+that.name+'. The server\'s response does not appear to be a valid DescribeProcess document. \ <p>This error may also indicate that the processing server you specified is temporarily unavailable or there is a \ problem with your internet connection. Try opening the processing server again, and if the problem persists, please report it by \ sending an email to <a href="mailto:'+that.terria.supportEmail+'">'+that.terria.supportEmail+'</a>.</p>' }); } var json = xml2json(xml); if (!defined(json.ProcessDescription)) { throw new TerriaError({ sender: that, title: 'Process does not have a process description', message: 'The WPS DescribeProcess for this process does not include a ProcessDescription.' }); } that._storeSupported = json.ProcessDescription.storeSupported === 'true'; that._statusSupported = json.ProcessDescription.statusSupported === 'true'; function throwNoInputs() { throw new TerriaError({ sender: that, title: 'Process does not have any inputs', message: 'This WPS process does not specify any inputs.' }); } 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 = 'This is the result of invoking the ' + this.name + ' process or service at ' + timestamp + ' with the input parameters below.'; 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: 'Unsupported parameter type', message: 'The parameter ' + input.Identifier + ' is not a supported type of parameter.' }); } function htmlEscapeText(text) { return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); } 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: 'Invalid WPS server response', message: '\ An error occurred while accessing the status location on the WPS server for process name '+catalogFunction.name+'. The server\'s response does not appear to be a valid ExecuteResponse document. \ <p>This error may also indicate that the processing server you specified is temporarily unavailable or there is a \ problem with your internet connection. If the problem persists, please report it by \ sending an email to <a href="mailto:'+catalogFunction.terria.supportEmail+'">'+catalogFunction.terria.supportEmail+'</a>.</p>' }); } var json = xml2json(xmlResponse); var status = json.Status; if (!defined(status)) { throw new TerriaError({ sender: catalogFunction, title: 'Invalid response from WPS server', message: 'The response from the WPS server does not include a Status element.' }); } if (defined(status.ProcessFailed)) { var errorMessage = 'The reason for failure is unknown.'; 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 = 'Web Processing Service invocation failed. More details are available on the Info panel.'; asyncResult.moreFailureDetailsAvailable = true; asyncResult.info.push({ name: 'Error Details', 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; 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;