diff --git a/examples_src/vector-layer.js b/examples_src/vector-layer.js
index 390ffe8081..633825e3b2 100644
--- a/examples_src/vector-layer.js
+++ b/examples_src/vector-layer.js
@@ -1,9 +1,9 @@
goog.require('ol.FeatureOverlay');
goog.require('ol.Map');
goog.require('ol.View');
+goog.require('ol.format.GeoJSON');
goog.require('ol.layer.Tile');
goog.require('ol.layer.Vector');
-goog.require('ol.source.GeoJSON');
goog.require('ol.source.MapQuest');
goog.require('ol.style.Fill');
goog.require('ol.style.Stroke');
@@ -31,10 +31,11 @@ var style = new ol.style.Style({
})
});
var styles = [style];
+
var vectorLayer = new ol.layer.Vector({
- source: new ol.source.GeoJSON({
- projection: 'EPSG:3857',
- url: 'data/geojson/countries.geojson'
+ source: new ol.source.Vector({
+ url: 'data/geojson/countries.geojson',
+ format: new ol.format.GeoJSON()
}),
style: function(feature, resolution) {
style.getText().setText(resolution < 5000 ? feature.get('name') : '');
diff --git a/externs/olx.js b/externs/olx.js
index 5cc76deff0..3591e56cc1 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -4883,9 +4883,11 @@ olx.source.TileWMSOptions.prototype.wrapX;
/**
* @typedef {{attributions: (Array.
|undefined),
* features: (Array.|undefined),
+ * format: (ol.format.Feature|undefined),
+ * loader: (ol.FeatureLoader|undefined),
* logo: (string|olx.LogoOptions|undefined),
- * projection: ol.proj.ProjectionLike,
- * state: (ol.source.State|string|undefined)}}
+ * strategy: (ol.LoadingStrategy|undefined),
+ * url: (string|undefined)}}
* @api
*/
olx.source.VectorOptions;
@@ -4907,6 +4909,25 @@ olx.source.VectorOptions.prototype.attributions;
olx.source.VectorOptions.prototype.features;
+/**
+ * The feature format used by the XHR loader when `url` is set. Required
+ * if `url` is set, otherwise ignored. Default is `undefined`.
+ * `url`
+ * @type {ol.format.Feature|undefined}
+ * @api
+ */
+olx.source.VectorOptions.prototype.format;
+
+
+/**
+ * The loader function used to load features, from a remote source for example.
+ * Note that the source will create and use an XHR loader when `src` is set.
+ * @type {ol.FeatureLoader|undefined}
+ * @api
+ */
+olx.source.VectorOptions.prototype.loader;
+
+
/**
* Logo.
* @type {string|olx.LogoOptions|undefined}
@@ -4916,19 +4937,23 @@ olx.source.VectorOptions.prototype.logo;
/**
- * Projection.
- * @type {ol.proj.ProjectionLike}
+ * The loading strategy to use. By default an {@link ol.loadingstrategy.all}
+ * strategy is used, which means that features at loaded all at once, and
+ * once.
+ * @type {ol.LoadingStrategy|undefined}
* @api
*/
-olx.source.VectorOptions.prototype.projection;
+olx.source.VectorOptions.prototype.strategy;
/**
- * State.
- * @type {ol.source.State|string|undefined}
+ * Set this option if you want the source to download features all at once
+ * and once for good. Internally the source uses an XHR feature loader (see
+ * {@link ol.featureloader.xhr}). Requires `format` to be set as well.
+ * @type {string|undefined}
* @api
*/
-olx.source.VectorOptions.prototype.state;
+olx.source.VectorOptions.prototype.url;
/**
diff --git a/src/ol/featureloader.js b/src/ol/featureloader.js
new file mode 100644
index 0000000000..2fc5dfe114
--- /dev/null
+++ b/src/ol/featureloader.js
@@ -0,0 +1,116 @@
+goog.provide('ol.FeatureLoader');
+goog.provide('ol.featureloader');
+
+goog.require('goog.asserts');
+goog.require('goog.events');
+goog.require('goog.net.EventType');
+goog.require('goog.net.XhrIo');
+goog.require('goog.net.XhrIo.ResponseType');
+goog.require('ol.format.FormatType');
+goog.require('ol.xml');
+
+
+/**
+ * @typedef {function(this:ol.source.Vector, ol.Extent, number,
+ * ol.proj.Projection)}
+ */
+ol.FeatureLoader;
+
+
+/**
+ * @param {string} url Feature URL service.
+ * @param {ol.format.Feature} format Feature format.
+ * @param {function(this:ol.source.Vector, Array.)} success
+ * Function called with the loaded features. Called with the vector
+ * source as `this`.
+ * @return {ol.FeatureLoader} The feature loader.
+ */
+ol.featureloader.loadFeaturesXhr = function(url, format, success) {
+ return (
+ /**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {ol.proj.Projection} projection Projection.
+ * @this {ol.source.Vector}
+ */
+ function(extent, resolution, projection) {
+ var xhrIo = new goog.net.XhrIo();
+ var type = format.getType();
+ var responseType;
+ // FIXME maybe use ResponseType.DOCUMENT?
+ if (type == ol.format.FormatType.BINARY &&
+ ol.has.ARRAY_BUFFER) {
+ responseType = goog.net.XhrIo.ResponseType.ARRAY_BUFFER;
+ } else {
+ responseType = goog.net.XhrIo.ResponseType.TEXT;
+ }
+ xhrIo.setResponseType(responseType);
+ goog.events.listen(xhrIo, goog.net.EventType.COMPLETE,
+ /**
+ * @param {Event} event Event.
+ * @private
+ * @this {ol.source.Vector}
+ */
+ function(event) {
+ var xhrIo = event.target;
+ goog.asserts.assertInstanceof(xhrIo, goog.net.XhrIo,
+ 'event.target/xhrIo is an instance of goog.net.XhrIo');
+ if (xhrIo.isSuccess()) {
+ var type = format.getType();
+ /** @type {ArrayBuffer|Document|Node|Object|string|undefined} */
+ var source;
+ if (type == ol.format.FormatType.BINARY &&
+ ol.has.ARRAY_BUFFER) {
+ source = xhrIo.getResponse();
+ goog.asserts.assertInstanceof(source, ArrayBuffer,
+ 'source is an instance of ArrayBuffer');
+ } else if (type == ol.format.FormatType.JSON) {
+ source = xhrIo.getResponseText();
+ } else if (type == ol.format.FormatType.TEXT) {
+ source = xhrIo.getResponseText();
+ } else if (type == ol.format.FormatType.XML) {
+ if (!goog.userAgent.IE) {
+ source = xhrIo.getResponseXml();
+ }
+ if (!goog.isDefAndNotNull(source)) {
+ source = ol.xml.parse(xhrIo.getResponseText());
+ }
+ } else {
+ goog.asserts.fail('unexpected format type');
+ }
+ if (goog.isDefAndNotNull(source)) {
+ var features = format.readFeatures(source,
+ {featureProjection: projection});
+ success.call(this, features);
+ } else {
+ goog.asserts.fail('undefined or null source');
+ }
+ } else {
+ // FIXME
+ }
+ goog.dispose(xhrIo);
+ }, false, this);
+ xhrIo.send(url);
+ });
+};
+
+
+/**
+ * Create an XHR feature loader for a `url` and `format`. The feature loader
+ * loads features (with XHR), parses the features, and adds them to the
+ * vector source.
+ * @param {string} url Feature URL service.
+ * @param {ol.format.Feature} format Feature format.
+ * @return {ol.FeatureLoader} The feature loader.
+ * @api
+ */
+ol.featureloader.xhr = function(url, format) {
+ return ol.featureloader.loadFeaturesXhr(url, format,
+ /**
+ * @param {Array.} features The loaded features.
+ * @this {ol.source.Vector}
+ */
+ function(features) {
+ this.addFeatures(features);
+ });
+};
diff --git a/src/ol/loadingstrategy.js b/src/ol/loadingstrategy.js
index 418fc62282..1437d29c7b 100644
--- a/src/ol/loadingstrategy.js
+++ b/src/ol/loadingstrategy.js
@@ -1,8 +1,15 @@
+goog.provide('ol.LoadingStrategy');
goog.provide('ol.loadingstrategy');
goog.require('ol.TileCoord');
+/**
+ * @typedef {function(ol.Extent, number): Array.}
+ */
+ol.LoadingStrategy;
+
+
/**
* Strategy function for loading all features with a single request.
* @param {ol.Extent} extent Extent.
diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js
index 53446596bf..393a276c34 100644
--- a/src/ol/source/vectorsource.js
+++ b/src/ol/source/vectorsource.js
@@ -12,9 +12,16 @@ goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.object');
+goog.require('ol.Extent');
+goog.require('ol.FeatureLoader');
+goog.require('ol.LoadingStrategy');
goog.require('ol.ObjectEventType');
+goog.require('ol.extent');
+goog.require('ol.featureloader');
+goog.require('ol.loadingstrategy');
goog.require('ol.proj');
goog.require('ol.source.Source');
+goog.require('ol.source.State');
goog.require('ol.structs.RBush');
@@ -71,17 +78,44 @@ ol.source.Vector = function(opt_options) {
goog.base(this, {
attributions: options.attributions,
logo: options.logo,
- projection: options.projection,
- state: goog.isDef(options.state) ?
- /** @type {ol.source.State} */ (options.state) : undefined
+ projection: undefined,
+ state: ol.source.State.READY
});
+ /**
+ * @private
+ * @type {ol.FeatureLoader}
+ */
+ this.loader_ = goog.nullFunction;
+
+ if (goog.isDef(options.loader)) {
+ this.loader_ = options.loader;
+ } else if (goog.isDef(options.url)) {
+ goog.asserts.assert(goog.isDef(options.format),
+ 'format must be set when url is set');
+ // create a XHR feature loader for "url" and "format"
+ this.loader_ = ol.featureloader.xhr(options.url, options.format);
+ }
+
+ /**
+ * @private
+ * @type {ol.LoadingStrategy}
+ */
+ this.strategy_ = goog.isDef(options.strategy) ? options.strategy :
+ ol.loadingstrategy.all;
+
/**
* @private
* @type {ol.structs.RBush.}
*/
this.rBush_ = new ol.structs.RBush();
+ /**
+ * @private
+ * @type {ol.structs.RBush.<{extent: ol.Extent}>}
+ */
+ this.loadedExtentsRtree_ = new ol.structs.RBush();
+
/**
* @private
* @type {Object.}
@@ -261,6 +295,7 @@ ol.source.Vector.prototype.clear = function(opt_fast) {
}
this.rBush_.clear();
+ this.loadedExtentsRtree_.clear();
this.nullGeometryFeatures_ = {};
var clearEvent = new ol.source.VectorEvent(ol.source.VectorEventType.CLEAR);
@@ -577,7 +612,27 @@ ol.source.Vector.prototype.isEmpty = function() {
* @param {number} resolution Resolution.
* @param {ol.proj.Projection} projection Projection.
*/
-ol.source.Vector.prototype.loadFeatures = goog.nullFunction;
+ol.source.Vector.prototype.loadFeatures = function(
+ extent, resolution, projection) {
+ var loadedExtentsRtree = this.loadedExtentsRtree_;
+ var extentsToLoad = this.strategy_(extent, resolution);
+ var i, ii;
+ for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
+ var extentToLoad = extentsToLoad[i];
+ var alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
+ /**
+ * @param {{extent: ol.Extent}} object Object.
+ * @return {boolean} Contains.
+ */
+ function(object) {
+ return ol.extent.containsExtent(object.extent, extentToLoad);
+ });
+ if (!alreadyLoaded) {
+ this.loader_.call(this, extentToLoad, resolution, projection);
+ loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
+ }
+ }
+};
/**
diff --git a/test/spec/ol/data/point.json b/test/spec/ol/data/point.json
new file mode 100644
index 0000000000..9ab34adfc1
--- /dev/null
+++ b/test/spec/ol/data/point.json
@@ -0,0 +1 @@
+{"type":"FeatureCollection","features":[{"type":"Feature","id":"01","properties":{},"geometry":{"type":"Point","coordinates":[-87.359296,35.001181]}}]}
diff --git a/test/spec/ol/featureloader.test.js b/test/spec/ol/featureloader.test.js
new file mode 100644
index 0000000000..6ce714c98e
--- /dev/null
+++ b/test/spec/ol/featureloader.test.js
@@ -0,0 +1,30 @@
+goog.provide('ol.test.featureloader');
+
+describe('ol.featureloader', function() {
+ describe('ol.featureloader.xhr', function() {
+ var loader;
+ var source;
+
+ beforeEach(function() {
+ var url = 'spec/ol/data/point.json';
+ var format = new ol.format.GeoJSON();
+
+ loader = ol.featureloader.xhr(url, format);
+ source = new ol.source.Vector();
+ });
+
+ it('adds features to the source', function(done) {
+ source.on(ol.source.VectorEventType.ADDFEATURE, function(e) {
+ expect(source.getFeatures().length).to.be.greaterThan(0);
+ done();
+ });
+ loader.call(source, [], 1, 'EPSG:3857');
+ });
+
+ });
+});
+
+goog.require('ol.featureloader');
+goog.require('ol.format.GeoJSON');
+goog.require('ol.source.Vector');
+goog.require('ol.source.VectorEventType');