diff --git a/examples/wps-client.html b/examples/wps-client.html index b588f7fba2..379f1bb990 100644 --- a/examples/wps-client.html +++ b/examples/wps-client.html @@ -22,7 +22,9 @@

WPS Client Example

-

This example shows how simple it is to use the WPS Client. See +

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 index dee7e5c50d..304c59271a 100644 --- a/examples/wps-client.js +++ b/examples/wps-client.js @@ -56,7 +56,7 @@ function init() { // the client directly if we are only dealing with a single process: /* client.execute({ - server: "local", + server: "opengeo", process: "JTS:intersection", // spatial input can be a feature or a geometry or an array of // features or geometries diff --git a/lib/OpenLayers/WPSClient.js b/lib/OpenLayers/WPSClient.js index 257e43166f..a335632219 100644 --- a/lib/OpenLayers/WPSClient.js +++ b/lib/OpenLayers/WPSClient.js @@ -8,16 +8,19 @@ */ /** + * @requires OpenLayers/Events.js * @requires OpenLayers/WPSProcess.js - * @requires OpenLayers/Format/WKT.js - * @requires OpenLayers/Format/GeoJSON.js * @requires OpenLayers/Format/WPSDescribeProcess.js - * @requires OpenLayers/Format/WPSExecute.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({ @@ -28,11 +31,18 @@ OpenLayers.WPSClient = OpenLayers.Class({ * Properties: * url - {String} the url of the server * version - {String} WPS version of the server - * describeProcessResponse - {Object} Cache of raw DescribeProcess + * 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 @@ -40,6 +50,18 @@ OpenLayers.WPSClient = OpenLayers.Class({ */ 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 * @@ -67,18 +89,22 @@ OpenLayers.WPSClient = OpenLayers.Class({ */ 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: '1.0.0', - describeProcessResponse: {} + 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. @@ -92,12 +118,14 @@ OpenLayers.WPSClient = OpenLayers.Class({ * 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 name 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. + * 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) { @@ -114,18 +142,18 @@ OpenLayers.WPSClient = OpenLayers.Class({ * Creates an . * * Parameters: - * server - {String} Local identifier from the servers that this instance + * serverID - {String} Local identifier from the servers that this instance * was constructed with. - * identifier - {String} Process identifier known to the server. + * processID - {String} Process identifier known to the server. * * Returns: * {} */ - getProcess: function(server, identifier) { + getProcess: function(serverID, processID) { var process = new OpenLayers.WPSProcess({ client: this, - server: server, - identifier: identifier + server: serverID, + identifier: processID }); if (!this.lazy) { process.describe(); @@ -133,6 +161,54 @@ OpenLayers.WPSClient = OpenLayers.Class({ 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); + } + }); + } + } else { + window.setTimeout(function() { + callback.call(scope, server.processDescription[processID]); + }, 0); + } + }, + CLASS_NAME: 'OpenLayers.WPSClient' }); diff --git a/lib/OpenLayers/WPSProcess.js b/lib/OpenLayers/WPSProcess.js index 749c3473a8..323d87bded 100644 --- a/lib/OpenLayers/WPSProcess.js +++ b/lib/OpenLayers/WPSProcess.js @@ -8,26 +8,27 @@ */ /** - * @requires OpenLayers/Events.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({ - /** - * APIProperty: events - * {} - * - * Supported event types: - * describeprocess - Fires when the process description is available for - * the first time. - */ - events: null, - /** * Property: client * {} The client that manages this process. @@ -52,6 +53,13 @@ OpenLayers.WPSProcess = OpenLayers.Class({ */ 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. @@ -60,7 +68,7 @@ OpenLayers.WPSProcess = OpenLayers.Class({ /** * Property: chained - * {Integer} Number of chained processes for pending execute reqeusts that + * {Integer} Number of chained processes for pending execute requests that * don't have a full configuration yet. */ chained: 0, @@ -79,18 +87,15 @@ OpenLayers.WPSProcess = OpenLayers.Class({ * 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.events = new OpenLayers.Events(this); + OpenLayers.Util.extend(this, options); this.executeCallbacks = []; - this.formats = { 'application/wkt': new OpenLayers.Format.WKT(), 'application/json': new OpenLayers.Format.GeoJSON() @@ -99,12 +104,10 @@ OpenLayers.WPSProcess = OpenLayers.Class({ /** * Method: describe - * Issues a DescribeProcess request asynchronously and fires the - * 'describeprocess' event as soon as the response is available in - * . + * Makes the client ssues a DescribeProcess request asynchronously. * * Parameters: - * options - {Object} Coniguration for the method call + * options - {Object} Configuration for the method call * * Available options: * callback - {Function} Callback to execute when the description is @@ -115,45 +118,21 @@ OpenLayers.WPSProcess = OpenLayers.Class({ */ describe: function(options) { options = options || {}; - function callback() { - if (options.callback) { - window.setTimeout(function() { - options.callback.call(options.scope, this.description); - }, 0); - } - } - var server = this.client.servers[this.server]; - if (this.description !== null) { - callback(); - return; - } else if (server.describeProcessResponse[this.identifier] === null) { - // pending request - this.events.register('describeprocess', this, callback); - return; - } else if (this.identifier in server.describeProcessResponse) { - // process description already cached on client - this.parseDescription(); - callback(); - return; - } - // set to null so we know a describeFeature request is pending - server.describeProcessResponse[this.identifier] = null; - OpenLayers.Request.GET({ - url: server.url, - params: { - SERVICE: 'WPS', - VERSION: server.version, - REQUEST: 'DescribeProcess', - IDENTIFIER: this.identifier - }, - success: function(response) { - this.parseDescription(response); + 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); } - }, - scope: this - }); + }, this); + } else if (options.callback) { + var description = this.description; + window.setTimeout(function() { + options.callback.call(options.scope, description); + }, 0); + } }, /** @@ -175,25 +154,22 @@ OpenLayers.WPSProcess = OpenLayers.Class({ * scope - {Object} Optional scope for the callback. */ configure: function(options) { - if (!this.description) { - this.describe({ - callback: function() { - this.configure(options); - }, - scope: this - }); - return; - } - var description = this.description, - inputs = options.inputs, - input, i, ii; - for (i=0, ii=description.dataInputs.length; i or an array of - * features. + * 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) { @@ -248,9 +224,12 @@ OpenLayers.WPSProcess = OpenLayers.Class({ ); //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[output.identifier] = features; + outputs[options.output || 'result'] = features; options.success.call(options.scope, outputs); } }, @@ -298,17 +277,13 @@ OpenLayers.WPSProcess = OpenLayers.Class({ * Parses the DescribeProcess response * * Parameters: - * response - {Object} + * description - {Object} */ - parseDescription: function(response) { + parseDescription: function(description) { var server = this.client.servers[this.server]; - if (response) { - server.describeProcessResponse[this.identifier] = response.responseText; - } this.description = new OpenLayers.Format.WPSDescribeProcess() - .read(server.describeProcessResponse[this.identifier]) + .read(server.processDescription[this.identifier]) .processDescriptions[this.identifier]; - this.events.triggerEvent('describeprocess'); }, /** @@ -331,9 +306,7 @@ OpenLayers.WPSProcess = OpenLayers.Class({ input.reference = { method: 'POST', href: data.process.server === this.server ? - //TODO what about implementations other than GeoServer? - 'http://geoserver/wps' : - this.client.servers[data.process.server].url + this.localWPS : this.client.servers[data.process.server].url }; data.process.describe({ callback: function() { @@ -415,7 +388,7 @@ OpenLayers.WPSProcess = OpenLayers.Class({ * * Parameters: * input - {Object} The dataInput that the chained process provides. - * chainLink - {} The process to chain. */ chainProcess: function(input, chainLink) { var output = this.getOutputIndex( diff --git a/tests/WPSClient.html b/tests/WPSClient.html index bd1eb477bc..2a9bc892b6 100644 --- a/tests/WPSClient.html +++ b/tests/WPSClient.html @@ -6,7 +6,7 @@ var client; function test_initialize(t) { - t.plan(2); + t.plan(3); client = new OpenLayers.WPSClient({ servers: { @@ -15,6 +15,7 @@ }); t.ok(client instanceof OpenLayers.WPSClient, 'creates an instance'); + t.ok(client.events, 'has an events instance'); t.eq(client.servers.local.url, '/geoserver/wps', 'servers stored on instance'); } @@ -36,6 +37,35 @@ } + function test_describeProcess(t) { + t.plan(6); + var log = {request: [], event: []}; + var originalGET = OpenLayers.Request.GET; + OpenLayers.Request.GET = function(cfg) { + log.request.push(cfg); + } + function describe(evt) { + log.event.push(evt); + } + client.events.register('describeprocess', this, describe); + + process = client.getProcess('local', 'gs:splitPolygon'); + t.eq(client.servers.local.processDescription['gs:splitPolyon'], null, 'describeProcess pending'); + process.describe(); + t.eq(log.request.length, 1, 'describeProcess request only sent once'); + log.request[0].success.call(client, { + responseText: '' + }); + t.eq(log.event[0].type, 'describeprocess', 'describeprocess event triggered'); + t.ok(client.servers.local.processDescription['gs:splitPolygon'], 'We have a process description!'); + process.describe(); + t.eq(log.request.length, 1, 'describeProcess request only sent once'); + t.eq(log.event.length, 1, 'describeprocess event only triggered once'); + + OpenLayers.Request.GET = originalGET; + client.events.unregister('describeprocess', this, describe); + } + function test_execute(t) { t.plan(1); diff --git a/tests/WPSProcess.html b/tests/WPSProcess.html index 44d81b3801..856b3c8aa6 100644 --- a/tests/WPSProcess.html +++ b/tests/WPSProcess.html @@ -9,54 +9,38 @@ local: 'geoserver/wps' } }); - client.servers.local.describeProcessResponse = { + client.servers.local.processDescription = { 'JTS:intersection': '' + 'JTS:intersectionReturns the intersectoin between a and b (eventually an empty collection if there is no intersection)Returns the intersectoin between a and b (eventually an empty collection if there is no intersection)aa[undescribed]text/xml; subtype=gml/3.1.1text/xml; subtype=gml/3.1.1text/xml; subtype=gml/2.1.2application/wktapplication/gml-3.1.1application/gml-2.1.2bb[undescribed]text/xml; subtype=gml/3.1.1text/xml; subtype=gml/3.1.1text/xml; subtype=gml/2.1.2application/wktapplication/gml-3.1.1application/gml-2.1.2resultProcess resulttext/xml; subtype=gml/3.1.1text/xml; subtype=gml/3.1.1text/xml; subtype=gml/2.1.2application/wktapplication/gml-3.1.1application/gml-2.1.2' }; function test_initialize(t) { - t.plan(2); - + t.plan(1); process = new OpenLayers.WPSProcess(); - t.ok(process instanceof OpenLayers.WPSProcess, 'creates an instance'); - t.ok(process.events, 'has an events instance'); } function test_describe(t) { - t.plan(6); - var log = {request: [], event: []}; - var originalGET = OpenLayers.Request.GET; - OpenLayers.Request.GET = function(cfg) { - log.request.push(cfg); - } - - process = client.getProcess('local', 'gs:splitPolygon'); - process.events.register('describeprocess', this, function(evt) { - log.event.push(evt); + t.plan(2); + process = client.getProcess('local', 'JTS:intersection'); + var log = []; + process.describe({ + callback: function(description) { log.push(description); } }); - t.eq(client.servers.local.describeProcessResponse['gs:splitPolyon'], null, 'describeProcess pending'); - process.describe(); - t.eq(log.request.length, 1, 'describeProcess request only sent once'); - log.request[0].success.call(process, { - responseText: '' + t.delay_call(0.1, function() { + t.eq(log.length, 1, 'callback called'); + t.eq(log[0].identifier, 'JTS:intersection', 'callback called with correct description'); }); - t.eq(log.event[0].type, 'describeprocess', 'describeprocess event triggered'); - t.ok(client.servers.local.describeProcessResponse['gs:splitPolygon'], 'We have a process description!'); - process.describe(); - t.eq(log.request.length, 1, 'describeProcess request only sent once'); - t.eq(log.event.length, 1, 'describeprocess event only triggered once'); - - OpenLayers.Request.GET = originalGET; } function test_execute(t) { - t.plan(5); + t.plan(7); var log = []; var originalPOST = OpenLayers.Request.POST; OpenLayers.Request.POST = function(cfg) { log.push(cfg); + cfg.success.call(cfg.scope, {responseText: ''}); } process = new OpenLayers.WPSProcess({ @@ -81,7 +65,7 @@ } }], processOutputs: [{ - identifier: 'result', + identifier: 'foo', complexOutput: { supported: { formats: {'application/wkt': true} @@ -90,26 +74,44 @@ }] }; var line = 'LINESTRING(117 22,112 18,118 13,115 8)'; + var polygon = 'POLYGON((110 20,120 20,120 10,110 10,110 20),(112 17,118 18,118 16,112 15,112 17))'; + var output = []; + function success(result) { + output.push(result); + } + // configured with output identifier process.execute({ inputs: { line: new OpenLayers.Format.WKT().read(line), - polygon: new OpenLayers.Format.WKT().read( - 'POLYGON((110 20,120 20,120 10,110 10,110 20),(112 17,118 18,118 16,112 15,112 17))' - ) - } + polygon: new OpenLayers.Format.WKT().read(polygon) + }, + output: 'foo', + success: success + }); + // configured without output identifier + process.execute({ + inputs: { + line: new OpenLayers.Format.WKT().read(line), + polygon: new OpenLayers.Format.WKT().read(polygon) + }, + success: success }); - t.eq(log.length, 1, 'execute request sent'); - t.eq(process.description.dataInputs[0].data.complexData.value, line, 'data for first input correct'); - t.eq(process.description.dataInputs[0].data.complexData.mimeType, 'application/wkt', 'format for first input correct'); - t.eq(process.description.responseForm.rawDataOutput.identifier, 'result', 'correct identifier for responseForm'); - t.eq(process.description.responseForm.rawDataOutput.mimeType, 'application/wkt', 'correct format for responseForm'); - - OpenLayers.Request.POST = originalPOST; + t.delay_call(0.1, function() { + t.eq(log.length, 2, 'Two execute requests sent'); + t.eq(process.description.dataInputs[0].data.complexData.value, line, 'data for first input correct'); + t.eq(process.description.dataInputs[0].data.complexData.mimeType, 'application/wkt', 'format for first input correct'); + t.eq(process.description.responseForm.rawDataOutput.identifier, 'foo', 'correct identifier for responseForm'); + t.eq(process.description.responseForm.rawDataOutput.mimeType, 'application/wkt', 'correct format for responseForm'); + t.ok('foo' in output[0], 'process result contains output with correct identifier when configured with output'); + t.ok('result' in output[1], 'process result contains output with correct identifier when configured without output'); + + OpenLayers.Request.POST = originalPOST; + }); } function test_chainProcess(t) { - t.plan(3); + t.plan(5); var originalGET = OpenLayers.Request.GET; OpenLayers.Request.GET = function(cfg) { @@ -137,27 +139,41 @@ } }); - var buffer = client.getProcess('local', 'JTS:buffer'); + // one buffer process to make sure chaining works + var buffer1 = client.getProcess('local', 'JTS:buffer'); + // another buffer process to make sure that things work asynchronously + var buffer2 = client.getProcess('local', 'JTS:buffer'); var log = []; - buffer.chainProcess = function() { + buffer1.chainProcess = buffer2.chainProcess = function() { log.push(this.executeCallbacks.length); OpenLayers.WPSProcess.prototype.chainProcess.apply(this, arguments); }; - var done = false; - buffer.execute({ + var done1 = done2 = false; + buffer1.execute({ inputs: { geom: intersect.output(), distance: 1 }, success: function(outputs) { - done = true; + done1 = true; + } + }); + buffer2.execute({ + inputs: { + geom: intersect.output(), + distance: 2 + }, + success: function(outputs) { + done2 = true; } }); t.delay_call(0.5, function() { - t.eq(log.length, 1, 'chainProcess called'); + t.eq(log.length, 2, 'chainProcess called once for each process'); t.eq(log[0], 1, 'executeCallback queued to wait for 1 chained process'); - t.eq(done, true, 'execute successfully completed'); + t.eq(log[1], 1, 'executeCallback queued to wait for 1 chained process'); + t.eq(done1, true, 'execute for buffer1 process successfully completed'); + t.eq(done2, true, 'execute for buffer2 process successfully completed'); OpenLayers.Request.GET = originalGET; OpenLayers.Request.POST = originalPOST;