diff --git a/examples/wps-client.html b/examples/wps-client.html new file mode 100644 index 0000000000..379f1bb990 --- /dev/null +++ b/examples/wps-client.html @@ -0,0 +1,31 @@ + + + + + + + OpenLayers WPS Client Example + + + + + + +

WPS Client Example

+ +
+ wps +
+ +
Shows the usage of the WPS Client
+ +
+ +
+

This example shows how simple it is to use the WPS Client. It + buffers an intersection of a geometry and a feature, which is + accomplished by chaining two processes. See + wps-client.js to see how this is done.

+
+ + diff --git a/examples/wps-client.js b/examples/wps-client.js new file mode 100644 index 0000000000..511d491ea0 --- /dev/null +++ b/examples/wps-client.js @@ -0,0 +1,75 @@ +OpenLayers.ProxyHost = 'proxy.cgi?url='; + +var map, client, intersect, buffer; + +function init() { + + map = new OpenLayers.Map('map', { + allOverlays: true, + center: [114, 16], + zoom: 4, + layers: [new OpenLayers.Layer.Vector()] + }); + + var features = [new OpenLayers.Feature.Vector(OpenLayers.Geometry.fromWKT( + 'LINESTRING(117 22,112 18,118 13, 115 8)' + ))]; + var geometry = OpenLayers.Geometry.fromWKT( + 'POLYGON((110 20,120 20,120 10,110 10,110 20),(112 17,118 18,118 16,112 15,112 17))' + ); + + map.baseLayer.addFeatures(features); + map.baseLayer.addFeatures([new OpenLayers.Feature.Vector(geometry)]); + + client = new OpenLayers.WPSClient({ + servers: { + opengeo: 'http://demo.opengeo.org/geoserver/wps' + } + }); + + // Create a process and configure it + intersect = client.getProcess('opengeo', 'JTS:intersection'); + intersect.configure({ + // spatial input can be a feature or a geometry or an array of + // features or geometries + inputs: { + a: features, + b: geometry + } + }); + + // Create another process which chains the previous one and execute it + buffer = client.getProcess('opengeo', 'JTS:buffer'); + buffer.execute({ + inputs: { + geom: intersect.output(), + distance: 1 + }, + success: function(outputs) { + // outputs.result is a feature or an array of features for spatial + // processes. + map.baseLayer.addFeatures(outputs.result); + } + }); + + // Instead of creating a process and executing it, we could call execute on + // the client directly if we are only dealing with a single process: + /* + client.execute({ + server: "opengeo", + process: "JTS:intersection", + // spatial input can be a feature or a geometry or an array of + // features or geometries + inputs: { + a: features, + b: geometry + }, + success: function(outputs) { + // outputs.result is a feature or an array of features for spatial + // processes. + map.baseLayer.addFeatures(outputs.result); + } + }); + */ + +} \ No newline at end of file diff --git a/examples/wps.html b/examples/wps.html index 84567fff87..b136e3a29d 100644 --- a/examples/wps.html +++ b/examples/wps.html @@ -50,7 +50,9 @@

WPS Builder Example

This example shows WPS in action by using the WPSCapabilities, WPSDescribeProcess and WPSExecute formats. See wps.js for the - source code.

+ source code. Note: For applications using WPS, the high level + approach shown in the wps-client example + is recommended instead.

  1. Select a process from the list below the map. The list is populated with the result of a WPS GetCapabilities request, parsed diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index c8c80d847f..cca76a78d8 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -394,7 +394,9 @@ "OpenLayers/Symbolizer/Raster.js", "OpenLayers/Lang.js", "OpenLayers/Lang/en.js", - "OpenLayers/Spherical.js" + "OpenLayers/Spherical.js", + "OpenLayers/WPSClient.js", + "OpenLayers/WPSProcess.js" ]; // etc. } diff --git a/lib/OpenLayers/WPSClient.js b/lib/OpenLayers/WPSClient.js new file mode 100644 index 0000000000..dd36c3c2c1 --- /dev/null +++ b/lib/OpenLayers/WPSClient.js @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. + * + * @requires OpenLayers/SingleFile.js + */ + +/** + * @requires OpenLayers/Events.js + * @requires OpenLayers/WPSProcess.js + * @requires OpenLayers/Format/WPSDescribeProcess.js + * @requires OpenLayers/Request.js + */ + +/** + * Class: OpenLayers.WPSClient + * High level API for interaction with Web Processing Services (WPS). + * An instance is used to create + * instances for servers known to the WPSClient. The WPSClient also caches + * DescribeProcess responses to reduce the number of requests sent to servers + * when processes are created. + */ +OpenLayers.WPSClient = OpenLayers.Class({ + + /** + * Property: servers + * {Object} Service metadata, keyed by a local identifier. + * + * Properties: + * url - {String} the url of the server + * version - {String} WPS version of the server + * processDescription - {Object} Cache of raw DescribeProcess + * responses, keyed by process identifier. + */ + servers: null, + + /** + * Property: version + * {String} The default WPS version to use if none is configured. Default + * is '1.0.0'. + */ + version: '1.0.0', + + /** + * Property: lazy + * {Boolean} Should the DescribeProcess be deferred until a process is + * fully configured? Default is false. + */ + lazy: false, + + /** + * Property: events + * {} + * + * Supported event types: + * describeprocess - Fires when the process description is available. + * Listeners receive an object with a 'raw' property holding the raw + * DescribeProcess response, and an 'identifier' property holding the + * process identifier of the described process. + */ + events: null, + + /** + * Constructor: OpenLayers.WPSClient + * + * Parameters: + * options - {Object} Object whose properties will be set on the instance. + * + * Avaliable options: + * servers - {Object} Mandatory. Service metadata, keyed by a local + * identifier. Can either be a string with the service url or an + * object literal with additional metadata: + * + * (code) + * servers: { + * local: '/geoserver/wps' + * }, { + * opengeo: { + * url: 'http://demo.opengeo.org/geoserver/wps', + * version: '1.0.0' + * } + * } + * (end) + * + * lazy - {Boolean} Optional. Set to true if DescribeProcess should not be + * requested until a process is fully configured. Default is false. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.events = new OpenLayers.Events(this); + this.servers = {}; + for (var s in options.servers) { + this.servers[s] = typeof options.servers[s] == 'string' ? { + url: options.servers[s], + version: this.version, + processDescription: {} + } : options.servers[s]; + } + }, + + /** + * APIMethod: execute + * Shortcut to execute a process with a single function call. This is + * equivalent to using and then calling execute on the + * process. + * + * Parameters: + * options - {Object} Options for the execute operation. + * + * Available options: + * server - {String} Mandatory. One of the local identifiers of the + * configured servers. + * process - {String} Mandatory. A process identifier known to the + * server. + * inputs - {Object} The inputs for the process, keyed by input identifier. + * For spatial data inputs, the value of an input is usually an + * , an or an array of + * geometries or features. + * output - {String} The identifier of an output to parse. Optional. If not + * provided, the first output will be parsed. + * success - {Function} Callback to call when the process is complete. + * This function is called with an outputs object as argument, which + * will have a property with the identifier of the requested output + * (e.g. 'result'). For processes that generate spatial output, the + * value will either be a single or an + * array of features. + * scope - {Object} Optional scope for the success callback. + */ + execute: function(options) { + var process = this.getProcess(options.server, options.process); + process.execute({ + inputs: options.inputs, + success: options.success, + scope: options.scope + }); + }, + + /** + * APIMethod: getProcess + * Creates an . + * + * Parameters: + * serverID - {String} Local identifier from the servers that this instance + * was constructed with. + * processID - {String} Process identifier known to the server. + * + * Returns: + * {} + */ + getProcess: function(serverID, processID) { + var process = new OpenLayers.WPSProcess({ + client: this, + server: serverID, + identifier: processID + }); + if (!this.lazy) { + process.describe(); + } + return process; + }, + + /** + * Method: describeProcess + * + * Parameters: + * serverID - {String} Identifier of the server + * processID - {String} Identifier of the requested process + * callback - {Function} Callback to call when the description is available + * scope - {Object} Optional execution scope for the callback function + */ + describeProcess: function(serverID, processID, callback, scope) { + var server = this.servers[serverID]; + if (!server.processDescription[processID]) { + if (!(processID in server.processDescription)) { + // set to null so we know a describeFeature request is pending + server.processDescription[processID] = null; + OpenLayers.Request.GET({ + url: server.url, + params: { + SERVICE: 'WPS', + VERSION: server.version, + REQUEST: 'DescribeProcess', + IDENTIFIER: processID + }, + success: function(response) { + server.processDescription[processID] = response.responseText; + this.events.triggerEvent('describeprocess', { + identifier: processID, + raw: response.responseText + }); + }, + scope: this + }); + } else { + // pending request + this.events.register('describeprocess', this, function describe(evt) { + if (evt.identifier === processID) { + this.events.unregister('describeprocess', this, describe); + callback.call(scope, evt.raw); + } + }); + } + } else { + window.setTimeout(function() { + callback.call(scope, server.processDescription[processID]); + }, 0); + } + }, + + /** + * Method: destroy + */ + destroy: function() { + this.events.destroy(); + this.events = null; + this.servers = null; + }, + + CLASS_NAME: 'OpenLayers.WPSClient' + +}); diff --git a/lib/OpenLayers/WPSProcess.js b/lib/OpenLayers/WPSProcess.js new file mode 100644 index 0000000000..0e7f6e22df --- /dev/null +++ b/lib/OpenLayers/WPSProcess.js @@ -0,0 +1,501 @@ +/** + * Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. + * + * @requires OpenLayers/SingleFile.js + */ + +/** + * @requires OpenLayers/Geometry.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Format/WKT.js + * @requires OpenLayers/Format/GeoJSON.js + * @requires OpenLayers/Format/WPSExecute.js + * @requires OpenLayers/Request.js + */ + +/** + * Class: OpenLayers.WPSProcess + * Representation of a WPS process. Usually instances of + * are created by calling 'getProcess' on an + * instance. + * + * Currently supports processes that have geometries + * or features as output, using WKT or GeoJSON as output format. It also + * supports chaining of processes by using the method to create a + * handle that is used as process input instead of a static value. + */ +OpenLayers.WPSProcess = OpenLayers.Class({ + + /** + * Property: client + * {} The client that manages this process. + */ + client: null, + + /** + * Property: server + * {String} Local client identifier for this process's server. + */ + server: null, + + /** + * Property: identifier + * {String} Process identifier known to the server. + */ + identifier: null, + + /** + * Property: description + * {Object} DescribeProcess response for this process. + */ + description: null, + + /** + * APIProperty: localWPS + * {String} Service endpoint for locally chained WPS processes. Default is + * 'http://geoserver/wps'. + */ + localWPS: 'http://geoserver/wps', + + /** + * Property: formats + * {Object} OpenLayers.Format instances keyed by mimetype. + */ + formats: null, + + /** + * Property: chained + * {Integer} Number of chained processes for pending execute requests that + * don't have a full configuration yet. + */ + chained: 0, + + /** + * Property: executeCallbacks + * {Array} Callbacks waiting to be executed until all chained processes + * are configured; + */ + executeCallbacks: null, + + /** + * Constructor: OpenLayers.WPSProcess + * + * Parameters: + * options - {Object} Object whose properties will be set on the instance. + * + * Avaliable options: + * client - {} Mandatory. Client that manages this + * process. + * server - {String} Mandatory. Local client identifier of this process's + * server. + * identifier - {String} Mandatory. Process identifier known to the server. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.executeCallbacks = []; + this.formats = { + 'application/wkt': new OpenLayers.Format.WKT(), + 'application/json': new OpenLayers.Format.GeoJSON() + }; + }, + + /** + * Method: describe + * Makes the client issue a DescribeProcess request asynchronously. + * + * Parameters: + * options - {Object} Configuration for the method call + * + * Available options: + * callback - {Function} Callback to execute when the description is + * available. Will be called with the parsed description as argument. + * Optional. + * scope - {Object} The scope in which the callback will be executed. + * Default is the global object. + */ + describe: function(options) { + options = options || {}; + if (!this.description) { + this.client.describeProcess(this.server, this.identifier, function(description) { + if (!this.description) { + this.parseDescription(description); + } + if (options.callback) { + options.callback.call(options.scope, this.description); + } + }, this); + } else if (options.callback) { + var description = this.description; + window.setTimeout(function() { + options.callback.call(options.scope, description); + }, 0); + } + }, + + /** + * APIMethod: configure + * Configure the process, but do not execute it. Use this for processes + * that are chained as input of a different process by means of the + * method. + * + * Parameters: + * options - {Object} + * + * Returns: + * {} this process. + * + * Available options: + * inputs - {Object} The inputs for the process, keyed by input identifier. + * For spatial data inputs, the value of an input is usually an + * , an or an array of + * geometries or features. + * callback - {Function} Callback to call when the configuration is + * complete. Optional. + * scope - {Object} Optional scope for the callback. + */ + configure: function(options) { + this.describe({ + callback: function() { + var description = this.description, + inputs = options.inputs, + input, i, ii; + for (i=0, ii=description.dataInputs.length; i, an or an array of + * geometries or features. + * output - {String} The identifier of the output to request and parse. + * Optional. If not provided, the first output will be requested. + * success - {Function} Callback to call when the process is complete. + * This function is called with an outputs object as argument, which + * will have a property with the identifier of the requested output + * (or 'result' if output was not configured). For processes that + * generate spatial output, the value will be an array of + * instances. + * scope - {Object} Optional scope for the success callback. + */ + execute: function(options) { + this.configure({ + inputs: options.inputs, + callback: function() { + var me = this; + //TODO For now we only deal with a single output + var outputIndex = this.getOutputIndex( + me.description.processOutputs, options.output + ); + me.setResponseForm({outputIndex: outputIndex}); + (function callback() { + OpenLayers.Util.removeItem(me.executeCallbacks, callback); + if (me.chained !== 0) { + // need to wait until chained processes have a + // description and configuration - see chainProcess + me.executeCallbacks.push(callback); + return; + } + // all chained processes are added as references now, so + // let's proceed. + OpenLayers.Request.POST({ + url: me.client.servers[me.server].url, + data: new OpenLayers.Format.WPSExecute().write(me.description), + success: function(response) { + var output = me.description.processOutputs[outputIndex]; + var mimeType = me.findMimeType( + output.complexOutput.supported.formats + ); + //TODO For now we assume a spatial output + var features = me.formats[mimeType].read(response.responseText); + if (features instanceof OpenLayers.Feature.Vector) { + features = [features]; + } + if (options.success) { + var outputs = {}; + outputs[options.output || 'result'] = features; + options.success.call(options.scope, outputs); + } + }, + scope: me + }); + })(); + }, + scope: this + }); + }, + + /** + * APIMethod: output + * Chain an output of a configured process (see ) as input to + * another process. + * + * (code) + * intersect = client.getProcess('opengeo', 'JTS:intersection'); + * intersect.configure({ + * // ... + * }); + * buffer = client.getProcess('opengeo', 'JTS:buffer'); + * buffer.execute({ + * inputs: { + * geom: intersect.output('result'), // <-- here we're chaining + * distance: 1 + * }, + * // ... + * }); + * (end) + * + * Parameters: + * identifier - {String} Identifier of the output that we're chaining. If + * not provided, the first output will be used. + */ + output: function(identifier) { + return new OpenLayers.WPSProcess.ChainLink({ + process: this, + output: identifier + }); + }, + + /** + * Method: parseDescription + * Parses the DescribeProcess response + * + * Parameters: + * description - {Object} + */ + parseDescription: function(description) { + var server = this.client.servers[this.server]; + this.description = new OpenLayers.Format.WPSDescribeProcess() + .read(server.processDescription[this.identifier]) + .processDescriptions[this.identifier]; + }, + + /** + * Method: setInputData + * Sets the data for a single input + * + * Parameters: + * input - {Object} An entry from the dataInputs array of the process + * description. + * data - {Mixed} For spatial data inputs, this is usually an + * , an or an array of + * geometries or features. + */ + setInputData: function(input, data) { + // clear any previous data + delete input.data; + delete input.reference; + if (data instanceof OpenLayers.WPSProcess.ChainLink) { + ++this.chained; + input.reference = { + method: 'POST', + href: data.process.server === this.server ? + this.localWPS : this.client.servers[data.process.server].url + }; + data.process.describe({ + callback: function() { + --this.chained; + this.chainProcess(input, data); + }, + scope: this + }); + } else { + input.data = {}; + var complexData = input.complexData; + if (complexData) { + var format = this.findMimeType(complexData.supported.formats); + input.data.complexData = { + mimeType: format, + value: this.formats[format].write(this.toFeatures(data)) + }; + } else { + input.data.literalData = { + value: data + }; + } + } + }, + + /** + * Method: setResponseForm + * Sets the responseForm property of the payload. + * + * Parameters: + * options - {Object} See below. + * + * Available options: + * outputIndex - {Integer} The index of the output to use. Optional. + * supportedFormats - {Object} Object with supported mime types as key, + * and true as value for supported types. Optional. + */ + setResponseForm: function(options) { + options = options || {}; + output = this.description.processOutputs[options.outputIndex || 0]; + this.description.responseForm = { + rawDataOutput: { + identifier: output.identifier, + mimeType: this.findMimeType(output.complexOutput.supported.formats, options.supportedFormats) + } + }; + }, + + /** + * Method: getOutputIndex + * Gets the index of a processOutput by its identifier + * + * Parameters: + * outputs - {Array} The processOutputs array to look at + * identifier - {String} The identifier of the output + * + * Returns + * {Integer} The index of the processOutput with the provided identifier + * in the outputs array. + */ + getOutputIndex: function(outputs, identifier) { + var output; + if (identifier) { + for (var i=outputs.length-1; i>=0; --i) { + if (outputs[i].identifier === identifier) { + output = i; + break; + } + } + } else { + output = 0; + } + return output; + }, + + /** + * Method: chainProcess + * Sets a fully configured chained process as input for this process. + * + * Parameters: + * input - {Object} The dataInput that the chained process provides. + * chainLink - {} The process to chain. + */ + chainProcess: function(input, chainLink) { + var output = this.getOutputIndex( + chainLink.process.description.processOutputs, chainLink.output + ); + input.reference.mimeType = this.findMimeType( + input.complexData.supported.formats, + chainLink.process.description.processOutputs[output].complexOutput.supported.formats + ); + var formats = {}; + formats[input.reference.mimeType] = true; + chainLink.process.setResponseForm({ + outputIndex: output, + supportedFormats: formats + }); + input.reference.body = chainLink.process.description; + while (this.executeCallbacks.length > 0) { + this.executeCallbacks[0](); + } + }, + + /** + * Method: toFeatures + * Converts spatial input into features so it can be processed by + * instances. + * + * Parameters: + * source - {Mixed} An , an + * , or an array of geometries or features + * + * Returns: + * {Array()} + */ + toFeatures: function(source) { + var isArray = OpenLayers.Util.isArray(source); + if (!isArray) { + source = [source]; + } + var target = new Array(source.length), + current; + for (var i=0, ii=source.length; i, but optional to check for + * supported mime types on a different target than this process. + * Default is to check against this process's supported formats. + * + * Returns: + * {String} A supported mime type. + */ + findMimeType: function(sourceFormats, targetFormats) { + targetFormats = targetFormats || this.formats; + for (var f in sourceFormats) { + if (f in targetFormats) { + return f; + } + } + }, + + CLASS_NAME: "OpenLayers.WPSProcess" + +}); + +/** + * Class: OpenLayers.WPSProcess.ChainLink + * Type for chaining processes. + */ +OpenLayers.WPSProcess.ChainLink = OpenLayers.Class({ + + /** + * Property: process + * {} The process to chain + */ + process: null, + + /** + * Property: output + * {String} The output identifier of the output we are going to use as + * input for another process. + */ + output: null, + + /** + * Constructor: OpenLayers.WPSProcess.ChainLink + * + * Parameters: + * options - {Object} Properties to set on the instance. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + }, + + CLASS_NAME: "OpenLayers.WPSProcess.ChainLink" + +}); diff --git a/tests/WPSClient.html b/tests/WPSClient.html new file mode 100644 index 0000000000..34b21f9362 --- /dev/null +++ b/tests/WPSClient.html @@ -0,0 +1,108 @@ + + + + + + + + diff --git a/tests/WPSProcess.html b/tests/WPSProcess.html new file mode 100644 index 0000000000..f668ca3d79 --- /dev/null +++ b/tests/WPSProcess.html @@ -0,0 +1,188 @@ + + + + + + + + diff --git a/tests/list-tests.html b/tests/list-tests.html index a019fd7e01..8c37464b84 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -232,6 +232,8 @@
  2. Kinetic.html
  3. Util.html
  4. Util/vendorPrefix.html
  5. +
  6. WPSClient.html
  7. +
  8. WPSProcess.html
  9. deprecated/Ajax.html
  10. deprecated/Util.html
  11. deprecated/BaseTypes/Class.html
  12. @@ -249,4 +251,4 @@
  13. deprecated/Renderer/SVG2.html
  14. deprecated/Layer/Yahoo.html
  15. deprecated/Tile/WFS.html
  16. - \ No newline at end of file +