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');