diff --git a/src/tile.js b/src/tile.js index 531d003082..99912225cc 100644 --- a/src/tile.js +++ b/src/tile.js @@ -17,10 +17,10 @@ var $ = require('jquery'); /** * This class defines the raw interface for a "tile" on a map. A tile is - * defined as a rectangular section of a map. The base implementation - * is independent of the actual content of the tile, but assumes that - * the content is loaded asynchronously via a url. The tile object - * has a promise-like interface. + * defined as a quadrilateral section of a map. The base implementation is + * independent of the actual content of the tile, but assumes that the content + * is loaded asynchronously via a url. The tile object has a promise-like + * interface. * @example * tile.then(function (data) {...}).catch(function (data) {...}); * @@ -48,6 +48,8 @@ var tile = function (spec) { * @property {object} index The tile index. * @property {number} index.x The tile x index. * @property {number} index.y The tile y index. + * @property {number} [index.level] The tile level index. + * @property {number} [index.reference] The tile reference index. * @name geo.tile#index */ Object.defineProperty(this, 'index', { @@ -143,13 +145,12 @@ var tile = function (spec) { /** * Return a unique string representation of the given tile useble as a hash - * key. Possibly extend later to include url information to make caches - * aware of the tile source. + * key. * * @returns {string} */ this.toString = function () { - return [this._index.level || 0, this._index.y, this._index.x].join('_'); + return [this._index.level || 0, this._index.y, this._index.x, this._index.reference || 0].join('_'); }; /** diff --git a/src/tileLayer.js b/src/tileLayer.js index 0bb161f735..5176b1c8af 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -18,12 +18,14 @@ var featureLayer = require('./featureLayer'); * zoom level. * @property {number} [cacheSize=400] The maximum number of tiles to cache. * The default is 200 if keepLower is false. + * @property {geo.fetchQueue} [queue] A fetch queue to use. If unspecified, a + * new queue is created. * @property {number} [queueSize=6] The queue size. Most browsers make at most * 6 requests to any domain, so this should be no more than 6 times the * number of subdomains used. * @property {number} [initialQueueSize=0] The initial queue size. `0` to use * the queue size. When querying a tile server that needs to load - * information before serving the first time, having an initial queue size of + * information before serving the first tile, having an initial queue size of * 1 can reduce the load on the tile server. After the initial queue of * tiles are loaded, the `queueSize` is used for all additional queries * unless the `initialQueueSize` is set again or the tile cache is reset. @@ -209,6 +211,7 @@ var tileLayer = function (arg) { m_initialQueueSize = arg.initialQueueSize || 0, m_lastTileSet = [], m_maxBounds = [], + m_reference, m_exited, m_this = this; @@ -230,7 +233,7 @@ var tileLayer = function (arg) { this._cache = tileCache({size: arg.cacheSize}); // initialize the tile fetch queue - this._queue = fetchQueue({ + this._queue = arg.queue || fetchQueue({ // this should probably be 6 * subdomains.length if subdomains are used size: m_queueSize, initialSize: m_initialQueueSize, @@ -239,9 +242,14 @@ var tileLayer = function (arg) { // smaller values will do needless computations. track: arg.cacheSize, needed: function (tile) { + if (this._tileLayers && this._tileLayers.length) { + return this._tileLayers.some((tl) => tile === tl.cache.get(tile.toString(), true)); + } return tile === m_this.cache.get(tile.toString(), true); } }); + this._queue._tileLayers = this._queue._tileLayers || []; + this._queue._tileLayers.push(m_this); var m_tileOffsetValues = {}; @@ -275,6 +283,30 @@ var tileLayer = function (arg) { return $.extend({}, m_this._activeTiles); // copy on output }}); + /** + * Get/set the queue object. + * @property {geo.fetchQueue} queue The current queue. + * @name geo.tileLayer#queue + */ + Object.defineProperty(this, 'queue', { + get: function () { return m_this._queue; }, + set: function (queue) { + /* The queue's needed function determines if a tile is still needed. A + * tile in the queue is needed if it is needed by at least one layer that + * is using it. _tileLayers tracks the layers that share the queue to + * allow walking through the layers and check if any layer needs a tile. + * When the queue is set, maintain the list of joined tile layers. */ + if (m_this._queue !== queue) { + if (this._queue && this._queue._tileLayers && this._queue._tileLayers.indexOf(m_this) >= 0) { + this._queue._tileLayers.splice(this._queue._tileLayers.indexOf(m_this), 1); + } + m_this._queue = queue; + m_this._queue._tileLayers = m_this._queue._tileLayers || []; + m_this._queue._tileLayers.push(m_this); + } + } + }); + /** * Get/set the queue size. * @property {number} size The queue size. @@ -302,6 +334,20 @@ var tileLayer = function (arg) { } }); + /** + * Get/set the tile reference value. + * @property {string} reference A reference value to distinguish tiles on + * this layer. + * @name geo.tileLayer#reference + */ + Object.defineProperty(this, 'reference', { + get: function () { return '' + m_this.id() + '_' + (m_reference || 0); }, + set: function (reference) { + m_reference = reference; + }, + configurable: true + }); + /** * The number of tiles at the given zoom level. The default implementation * just returns `Math.pow(2, z)`. @@ -566,11 +612,12 @@ var tileLayer = function (arg) { * @param {object} index The tile index * @param {number} index.x * @param {number} index.y - * @param {number} index.level + * @param {number} [index.level] + * @param {number} [index.reference] * @returns {string} */ this._tileHash = function (index) { - return [index.level || 0, index.y, index.x].join('_'); + return [index.level || 0, index.y, index.x, index.reference || 0].join('_'); }; /** @@ -663,8 +710,8 @@ var tileLayer = function (arg) { // loop over the tile range for (i = start.x; i <= end.x; i += 1) { for (j = start.y; j <= end.y; j += 1) { - index = {level: level, x: i, y: j}; - source = {level: level, x: i, y: j}; + index = {level: level, x: i, y: j, reference: m_this.reference}; + source = {level: level, x: i, y: j, reference: m_this.reference}; if (m_this._options.wrapX) { source.x = modulo(source.x, nTilesLevel.x); } @@ -1622,6 +1669,9 @@ var tileLayer = function (arg) { // call super method s_exit.apply(m_this, arguments); m_exited = true; + if (this._queue && this._queue._tileLayers && this._queue._tileLayers.indexOf(m_this) >= 0) { + this._queue._tileLayers.splice(this._queue._tileLayers.indexOf(m_this), 1); + } return m_this; }; diff --git a/tests/cases/osmLayer.js b/tests/cases/osmLayer.js index 6657bcffc1..3237b0814b 100644 --- a/tests/cases/osmLayer.js +++ b/tests/cases/osmLayer.js @@ -256,7 +256,7 @@ describe('geo.core.osmLayer', function () { it('check null tiles and switch to svg', function () { positions = {}; $.each($('[tile-reference]'), function () { - var ref = $(this).attr('tile-reference'); + var ref = $(this).attr('tile-reference').split('_').slice(0, 3).join('_'); positions[ref] = $(this)[0].getBoundingClientRect(); }); map.deleteLayer(layer); @@ -268,7 +268,7 @@ describe('geo.core.osmLayer', function () { }); it('compare tile offsets at angle ' + angle, function () { $.each($('image[reference]'), function () { - var ref = $(this).attr('reference'); + var ref = $(this).attr('reference').split('_').slice(0, 3).join('_'); /* Only check the top level */ if (ref.indexOf('4_') === 0) { var offset = $(this)[0].getBoundingClientRect(); diff --git a/tests/cases/tileLayer.js b/tests/cases/tileLayer.js index 55c5ae88cc..4653bdaf10 100644 --- a/tests/cases/tileLayer.js +++ b/tests/cases/tileLayer.js @@ -456,6 +456,26 @@ describe('geo.tileLayer', function () { expect(layer.initialQueueSize).toBe(1); expect(layer._queue.initialSize).toBe(1); }); + it('queue', function () { + var m = map(), layer; + opts.map = m; + layer = geo.tileLayer(opts); + expect(layer.queue._tileLayers.length).toBe(1); + var layer2 = geo.tileLayer(opts); + var origQueue = layer2.queue; + layer2.queue = layer.queue; + expect(layer.queue._tileLayers.length).toBe(2); + layer2.queue = origQueue; + expect(layer.queue._tileLayers.length).toBe(1); + }); + it('reference', function () { + var m = map(), layer; + opts.map = m; + layer = geo.tileLayer(opts); + expect(layer.reference).toBe(layer.id() + '_0'); + layer.reference = 'A'; + expect(layer.reference).toBe(layer.id() + '_A'); + }); }); describe('Public utility methods', function () { describe('isValid', function () { diff --git a/tutorials/multiframe/index.pug b/tutorials/multiframe/index.pug index ee3541b166..463054d30e 100644 --- a/tutorials/multiframe/index.pug +++ b/tutorials/multiframe/index.pug @@ -71,11 +71,13 @@ block mainTutorial } updating = true; // wait until all current tiles are loaded before loading more - map.onIdle(() => { + layerA.onIdle(() => { // load the new frame in the background layer - layerB._frame = frame; - layerB.url(`${baseUrl}${frame}`); - map.onIdle(() => { + if (frame !== layerB._frame) { + layerB._frame = frame; + layerB.url(`${baseUrl}${frame}`); + } + layerB.onIdle(() => { // once everything is loaded, check if we still need to swap layers updating = false; // the top layer is what we want so we don't have to do anything. @@ -122,6 +124,9 @@ block mainTutorial layerB = map.createLayer('osm', params.layer); // create a foreground layer for the current frame params.layer.url = `${baseUrl}0`; + // have the layers share a fetch queue. Since both tile layers are from + // the same server, this prevents requests from getting backlogged. + params.layer.queue = layerB.queue; layerA = map.createLayer('osm', params.layer); layerA._frame = 0; // adjust the frame slider and listen to changes