Adding a protocol for reading features cross-origin from services that support JSON with a callback. r=erilem (closes #2956)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@11691 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2011-03-10 20:51:03 +00:00
parent 545c001b5e
commit 27da366cd0
6 changed files with 710 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>OpenLayers Script Protocol Example</title>
<link rel="stylesheet" href="../theme/default/style.css" type="text/css">
<link rel="stylesheet" href="style.css" type="text/css">
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<script src="../lib/OpenLayers.js"></script>
</head>
<body>
<h1 id="title">Script Protocol</h1>
<div id="tags">
protocol, script, cross origin, advanced
</div>
<p id="shortdesc">
Demonstrates the use of a script protocol for making feature requests
cross origin.
</p>
<div id="map" class="smallmap"></div>
<div id="docs">
<p>
In cases where a service returns serialized features and accepts
a named callback (e.g. http://example.com/features.json?callback=foo),
the script protocol can be used to read features without being
restricted by the same origin policy.
</p>
<p>
View the <a href="cross-origin.js" target="_blank">cross-origin.js</a>
source to see how this is done
</p>
</div>
<script src="cross-origin.js"></script>
</body>
</html>

39
examples/cross-origin.js Normal file
View File

@@ -0,0 +1,39 @@
var map = new OpenLayers.Map({
div: "map",
layers: [
new OpenLayers.Layer.WMS(
"World Map",
"http://maps.opengeo.org/geowebcache/service/wms",
{layers: "bluemarble"}
),
new OpenLayers.Layer.Vector("States", {
strategies: [new OpenLayers.Strategy.BBOX()],
protocol: new OpenLayers.Protocol.Script({
url: "http://suite.opengeo.org/geoserver/wfs",
callbackKey: "format_options",
callbackPrefix: "callback:",
params: {
service: "WFS",
version: "1.1.0",
srsName: "EPSG:4326",
request: "GetFeature",
typeName: "world:cities",
outputFormat: "json"
},
filterToParams: function(filter, params) {
// example to demonstrate BBOX serialization
if (filter.type === OpenLayers.Filter.Spatial.BBOX) {
params.bbox = filter.value.toArray();
if (filter.projection) {
params.bbox.push(filter.projection.getCode());
}
}
return params;
}
})
})
],
center: new OpenLayers.LonLat(0, 0),
zoom: 1
});

View File

@@ -252,6 +252,7 @@
"OpenLayers/Protocol/WFS/v1.js", "OpenLayers/Protocol/WFS/v1.js",
"OpenLayers/Protocol/WFS/v1_0_0.js", "OpenLayers/Protocol/WFS/v1_0_0.js",
"OpenLayers/Protocol/WFS/v1_1_0.js", "OpenLayers/Protocol/WFS/v1_1_0.js",
"OpenLayers/Protocol/Script.js",
"OpenLayers/Protocol/SOS.js", "OpenLayers/Protocol/SOS.js",
"OpenLayers/Protocol/SOS/v1_0_0.js", "OpenLayers/Protocol/SOS/v1_0_0.js",
"OpenLayers/Layer/PointTrack.js", "OpenLayers/Layer/PointTrack.js",

View File

@@ -0,0 +1,363 @@
/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Protocol.js
* @requires OpenLayers/Feature/Vector.js
* @requires OpenLayers/Format/GeoJSON.js
*/
/**
* Class: OpenLayers.Protocol.Script
* A basic Script protocol for vector layers. Create a new instance with the
* <OpenLayers.Protocol.Script> constructor. A script protocol is used to
* get around the same origin policy. It works with services that return
* JSONP - that is, JSON wrapped in a client-specified callback. The
* protocol handles fetching and parsing of feature data and sends parsed
* features to the <callback> configured with the protocol. The protocol
* expects features serialized as GeoJSON by default, but can be configured
* to work with other formats by setting the <format> property.
*
* Inherits from:
* - <OpenLayers.Protocol>
*/
OpenLayers.Protocol.Script = OpenLayers.Class(OpenLayers.Protocol, {
/**
* APIProperty: url
* {String} Service URL. The service is expected to return serialized
* features wrapped in a named callback (where the callback name is
* generated by this protocol).
* Read-only, set through the options passed to the constructor.
*/
url: null,
/**
* APIProperty: params
* {Object} Query string parameters to be appended to the URL.
* Read-only, set through the options passed to the constructor.
* Example: {maxFeatures: 50}
*/
params: null,
/**
* APIProperty: callback
* {Object} Function to be called when the <read> operation completes.
*/
callback: null,
/**
* APIProperty: scope
* {Object} Optional ``this`` object for the callback. Read-only, set
* through the options passed to the constructor.
*/
scope: null,
/**
* APIProperty: format
* {<OpenLayers.Format>} Format for parsing features. Default is an
* <OpenLayers.Format.GeoJSON> format. If an alternative is provided,
* the format's read method must take an object and return an array
* of features.
*/
format: null,
/**
* APIProperty: callbackKey
* {String} The name of the query string parameter that the service
* recognizes as the callback identifier. Default is "callback".
* This key is used to generate the URL for the script. For example
* setting <callbackKey> to "myCallback" would result in a URL like
* http://example.com/?myCallback=...
*/
callbackKey: "callback",
/**
* APIProperty: callbackPrefix
* {String} Where a service requires that the callback query string
* parameter value is prefixed by some string, this value may be set.
* For example, setting <callbackPrefix> to "foo:" would result in a
* URL like http://example.com/?callback=foo:... Default is "".
*/
callbackPrefix: "",
/**
* Property: pendingRequests
* {Object} References all pending requests. Property names are script
* identifiers and property values are script elements.
*/
pendingRequests: null,
/**
* Constructor: OpenLayers.Protocol.Script
* A class for giving layers generic Script protocol.
*
* Parameters:
* options - {Object} Optional object whose properties will be set on the
* instance.
*
* Valid options include:
* url - {String}
* params - {Object}
* callback - {Function}
* scope - {Object}
*/
initialize: function(options) {
options = options || {};
this.params = {};
this.pendingRequests = {};
OpenLayers.Protocol.prototype.initialize.apply(this, arguments);
if (!this.format) {
this.format = new OpenLayers.Format.GeoJSON();
}
if (!this.filterToParams && OpenLayers.Protocol.simpleFilterSerializer) {
this.filterToParams = OpenLayers.Function.bind(
OpenLayers.Protocol.simpleFilterSerializer, this
);
}
},
/**
* APIMethod: read
* Construct a request for reading new features.
*
* Parameters:
* options - {Object} Optional object for configuring the request.
* This object is modified and should not be reused.
*
* Valid options:
* url - {String} Url for the request.
* params - {Object} Parameters to get serialized as a query string.
* filter - {<OpenLayers.Filter>} Filter to get serialized as a
* query string.
*
* Returns:
* {<OpenLayers.Protocol.Response>} A response object, whose "priv" property
* references the injected script. This object is also passed to the
* callback function when the request completes, its "features" property
* is then populated with the features received from the server.
*/
read: function(options) {
OpenLayers.Protocol.prototype.read.apply(this, arguments);
options = OpenLayers.Util.applyDefaults(options, this.options);
options.params = OpenLayers.Util.applyDefaults(
options.params, this.options.params
);
if (options.filter && this.filterToParams) {
options.params = this.filterToParams(
options.filter, options.params
);
}
var response = new OpenLayers.Protocol.Response({requestType: "read"});
var request = this.createRequest(
options.url,
options.params,
OpenLayers.Function.bind(function(data) {
response.data = data;
this.handleRead(response, options);
}, this)
);
response.priv = request;
return response;
},
/**
* APIMethod: filterToParams
* Optional method to translate an <OpenLayers.Filter> object into an object
* that can be serialized as request query string provided. If a custom
* method is not provided, any filter will not be serialized.
*
* Parameters:
* filter - {<OpenLayers.Filter>} filter to convert.
* params - {Object} The parameters object.
*
* Returns:
* {Object} The resulting parameters object.
*/
/**
* Method: createRequest
* Issues a request for features by creating injecting a script in the
* document head.
*
* Parameters:
* url - {String} Service URL.
* params - {Object} Query string parameters.
* callback - {Function} Callback to be called with resulting data.
*
* Returns:
* {HTMLScriptElement} The script pending execution.
*/
createRequest: function(url, params, callback) {
var id = OpenLayers.Protocol.Script.register(callback);
var name = "OpenLayers.Protocol.Script.getCallback(" + id + ")";
params = OpenLayers.Util.extend({}, params);
params[this.callbackKey] = this.callbackPrefix + name;
url = OpenLayers.Util.urlAppend(
url, OpenLayers.Util.getParameterString(params)
);
var script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.id = "OpenLayers_Protocol_Script_" + id;
this.pendingRequests[script.id] = script;
var head = document.getElementsByTagName("head")[0];
head.appendChild(script);
return script;
},
/**
* Method: destroyRequest
* Remove a script node associated with a response from the document. Also
* unregisters the callback and removes the script from the
* <pendingRequests> object.
*
* Parameters:
* script - {HTMLScriptElement}
*/
destroyRequest: function(script) {
OpenLayers.Protocol.Script.unregister(script.id.split("_").pop());
delete this.pendingRequests[script.id];
if (script.parentNode) {
script.parentNode.removeChild(script);
}
},
/**
* Method: handleRead
* Individual callbacks are created for read, create and update, should
* a subclass need to override each one separately.
*
* Parameters:
* response - {<OpenLayers.Protocol.Response>} The response object to pass to
* the user callback.
* options - {Object} The user options passed to the read call.
*/
handleRead: function(response, options) {
this.handleResponse(response, options);
},
/**
* Method: handleResponse
* Called by CRUD specific handlers.
*
* Parameters:
* response - {<OpenLayers.Protocol.Response>} The response object to pass to
* any user callback.
* options - {Object} The user options passed to the create, read, update,
* or delete call.
*/
handleResponse: function(response, options) {
if (options.callback) {
if (response.data) {
response.features = this.parseFeatures(response.data);
response.code = OpenLayers.Protocol.Response.SUCCESS;
} else {
response.code = OpenLayers.Protocol.Response.FAILURE;
}
this.destroyRequest(response.priv);
options.callback.call(options.scope, response);
}
},
/**
* Method: parseFeatures
* Read Script response body and return features.
*
* Parameters:
* data - {Object} The data sent to the callback function by the server.
*
* Returns:
* {Array({<OpenLayers.Feature.Vector>})} or
* {<OpenLayers.Feature.Vector>} Array of features or a single feature.
*/
parseFeatures: function(data) {
return this.format.read(data);
},
/**
* APIMethod: abort
* Abort an ongoing request. If no response is provided, all pending
* requests will be aborted.
*
* Parameters:
* response - {<OpenLayers.Protocol.Response>} The response object returned
* from a <read> request.
*/
abort: function(response) {
if (response) {
this.destroyRequest(response.priv);
} else {
for (var key in this.pendingRequests) {
this.destroyRequest(this.pendingRequests[key]);
}
}
},
/**
* APIMethod: destroy
* Clean up the protocol.
*/
destroy: function() {
this.abort();
delete this.params;
delete this.format;
OpenLayers.Protocol.prototype.destroy.apply(this);
},
CLASS_NAME: "OpenLayers.Protocol.Script"
});
(function() {
var o = OpenLayers.Protocol.Script;
var counter = 0;
var registry = {};
/**
* Function: OpenLayers.Protocol.Script.register
* Register a callback for a newly created script.
*
* Parameters:
* callback: {Function} The callback to be executed when the newly added
* script loads. This callback will be called with a single argument
* that is the JSON returned by the service.
*
* Returns:
* {Number} An identifier for retreiving the registered callback.
*/
o.register = function(callback) {
var id = ++counter;
registry[id] = callback;
return id;
};
/**
* Function: OpenLayers.Protocol.Script.unregister
* Unregister a callback previously registered with the register function.
*
* Parameters:
* id: {Number} The identifer returned by the register function.
*/
o.unregister = function(id) {
delete registry[id];
};
/**
* Function: OpenLayers.Protocol.Script.getCallback
* Retreive and unregister a callback. A call to this function is the "P"
* in JSONP. For example, a script may be added with a src attribute
* http://example.com/features.json?callback=OpenLayers.Protocol.Script.getCallback(1)
*
* Parameters:
* id: {Number} The identifer returned by the register function.
*/
o.getCallback = function(id) {
var callback = registry[id];
o.unregister(id);
return callback;
};
})();

271
tests/Protocol/Script.html Normal file
View File

@@ -0,0 +1,271 @@
<html>
<head>
<script src="../../lib/OpenLayers.js"></script>
<script type="text/javascript">
function test_constructor(t) {
t.plan(11);
var a = new OpenLayers.Protocol.Script({
url: "foo"
});
// 7 tests
t.eq(a.url, "foo", "constructor sets url");
t.eq(a.options.url, a.url, "constructor copies url to options.url");
t.eq(a.params, {}, "constructor sets params");
t.eq(a.options.params, undefined, "constructor does not copy params to options.params");
t.ok(a.format instanceof OpenLayers.Format.GeoJSON,
"constructor sets a GeoJSON format by default");
t.eq(a.callbackKey, 'callback',
"callbackKey is set to 'callback' by default");
t.eq(a.callbackPrefix, '',
"callbackPrefix is set to '' by default");
var params = {hello: "world"};
var b = new OpenLayers.Protocol.Script({
url: "bar",
params: params,
callbackKey: 'cb_key',
callbackPrefix: 'cb_prefix'
});
// 6 tests
t.eq(b.params, params, "constructor sets params");
t.eq(b.options.params, b.params, "constructor copies params to options.params");
t.eq(b.callbackKey, 'cb_key',
"callbackKey is set to 'cb_key'");
t.eq(b.callbackPrefix, 'cb_prefix',
"callbackPrefix is set to 'cb_prefix'");
}
function test_destroy(t) {
t.plan(3);
var aborted = false;
var protocol = new OpenLayers.Protocol.Script({
url: "bar",
params: {hello: "world"},
abort: function() {
aborted = true;
}
});
protocol.destroy();
t.ok(aborted, "destroy aborts request");
t.eq(protocol.params, null, "destroy nullifies params");
t.eq(protocol.format, null, "destroy nullifies format");
}
function test_read(t) {
t.plan(5);
var protocol = new OpenLayers.Protocol.Script({
'url': 'foo_url',
'params': {'k': 'foo_param'}
});
// fake XHR request object
var request = {'status': 200};
// options to pass to read
var readOptions = {
'url': 'bar_url',
'params': {'k': 'bar_param'}
};
var response;
protocol.createRequest = function(url, params, callback) {
// 4 tests
t.ok(this == protocol,
'createRequest called with correct scope');
t.ok(url == readOptions.url,
'createRequest called with correct url');
t.ok(params == readOptions.params,
'createRequest called with correct params');
t.ok(callback instanceof Function,
'createRequest called with a function as callback');
return 'foo_request';
};
var resp = protocol.read(readOptions);
t.eq(resp.priv, 'foo_request',
'response priv property set to what the createRequest method returns');
}
function test_read_bbox(t) {
t.plan(6);
var _createRequest = OpenLayers.Protocol.Script.prototype.createRequest;
var bounds = new OpenLayers.Bounds(1, 2, 3, 4);
var filter = new OpenLayers.Filter.Spatial({
type: OpenLayers.Filter.Spatial.BBOX,
value: bounds,
projection: new OpenLayers.Projection("foo")
});
// log requests
var log, exp;
OpenLayers.Protocol.Script.prototype.createRequest = function(url, params,
callback) {
log.push(params.bbox);
return null;
};
// 1) issue request with default protocol
log = [];
new OpenLayers.Protocol.Script().read({filter: filter});
t.eq(log.length, 1, "1) createRequest called once");
t.ok(log[0] instanceof Array, "1) bbox param is array");
exp = bounds.toArray();
t.eq(log[0], exp, "1) bbox param doesn't include SRS id by default");
// 2) issue request with default protocol
log = [];
new OpenLayers.Protocol.Script({srsInBBOX: true}).read({filter: filter});
t.eq(log.length, 1, "2) createRequest called once");
t.ok(log[0] instanceof Array, "2) bbox param is array");
exp = bounds.toArray();
exp.push("foo");
t.eq(log[0], exp, "2) bbox param includes SRS id if srsInBBOX is true");
OpenLayers.Protocol.Script.prototype.createRequest = _createRequest;
}
function test_createRequest(t) {
t.plan(3);
var protocol = new OpenLayers.Protocol.Script({
callbackKey: 'cb_key',
callbackPrefix: 'cb_prefix:'
});
var _register = OpenLayers.Protocol.Script.register;
OpenLayers.Protocol.Script.register = function() {
return 'bar';
};
var script = protocol.createRequest('http://bar_url/', {'k': 'bar_param'}, 'bar_callback');
t.eq(script.type, 'text/javascript',
'created script has a correct type');
t.eq(script.src, 'http://bar_url/?k=bar_param&cb_key=cb_prefix%3AOpenLayers.Protocol.Script.getCallback(bar)',
'created script has a correct url');
t.eq(script.id, 'OpenLayers_Protocol_Script_bar',
'created script has a correct id');
OpenLayers.Protocol.Script.register = _register;
}
function test_destroyRequest(t) {
t.plan(2);
var protocol = new OpenLayers.Protocol.Script({});
var _unregister = OpenLayers.Protocol.Script.unregister;
OpenLayers.Protocol.Script.unregister = function(id) {
t.eq(id, 'foo', "destroyRequest calls unregister with correct id");
};
var script = {
id: 'script_foo'
};
protocol.destroyRequest(script);
t.eq(protocol.pendingRequests[script.id], null,
"destroyRequest nullifies the pending request");
OpenLayers.Protocol.Script.unregister = _unregister;
}
function test_handleResponse(t) {
t.plan(8);
var protocol = new OpenLayers.Protocol.Script();
// 2 tests (should be called only twive)
protocol.destroyRequest = function(priv) {
t.eq(priv, 'foo_priv', 'destroyRequest called with correct argument');
}
// 1 test (should be called only once)
protocol.parseFeatures = function(data) {
t.eq(data, 'foo_data', 'parseFeatures called with correct argument');
return 'foo_features';
}
var response = {
priv: 'foo_priv',
data: 'foo_data'
}
var options = {
// 2 tests (should be called twice)
scope: 'foo_scope',
callback: function(resp) {
t.eq(this, 'foo_scope', 'callback called with correct scope');
}
}
protocol.handleResponse(response, options);
// 2 tests
t.eq(response.code, OpenLayers.Protocol.Response.SUCCESS,
'response code correctly set');
t.eq(response.features, 'foo_features',
'response features takes a correct value');
response = {
priv: 'foo_priv'
}
protocol.handleResponse(response, options);
// 1 test
t.eq(response.code, OpenLayers.Protocol.Response.FAILURE,
'response code correctly set');
}
function test_parseFeatures(t) {
t.plan(1);
var protocol = new OpenLayers.Protocol.Script();
protocol.format = {
'read': function(data) {
t.ok(true, 'format.read called');
}
};
var ret = protocol.parseFeatures({foo: 'bar'});
}
function test_abort(t) {
t.plan(2);
var protocol = new OpenLayers.Protocol.Script();
// 1 test
protocol.destroyRequest = function(priv) {
t.eq(priv, 'foo_priv', 'destroyRequest called with correct argument');
}
var response = {
priv: 'foo_priv'
}
protocol.abort(response);
var calls = [];
protocol.pendingRequests = {
'foo': 'foo_request',
'bar': 'bar_request'
}
protocol.destroyRequest = function(priv) {
calls.push(priv);
}
protocol.abort();
// 1 test
t.eq(calls, ['foo_request', 'bar_request'],
'destroyRequest called for each pending requests');
}
</script>
</head>
<body>
</body>
</html>

View File

@@ -177,6 +177,7 @@
<li>Projection.html</li> <li>Projection.html</li>
<li>Protocol.html</li> <li>Protocol.html</li>
<li>Protocol/HTTP.html</li> <li>Protocol/HTTP.html</li>
<li>Protocol/Script.html</li>
<li>Protocol/SimpleFilterSerializer.html</li> <li>Protocol/SimpleFilterSerializer.html</li>
<li>Protocol/SQL.html</li> <li>Protocol/SQL.html</li>
<li>Protocol/SQL/Gears.html</li> <li>Protocol/SQL/Gears.html</li>