From 2ee362a79bc38120b48ea69d65721019ecf73104 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 16 Nov 2012 16:19:59 +0100 Subject: [PATCH 01/14] New tile image cache and tile queue improvements We now reuse tile images by maintaining a cache of image elements with a simplified LRU expiry policy (by order, not by timestamp). The tile queue is bypassed for images that are available in the cache, so they can be rendered immediately. And the tile queue itself loads more than just one image at a time now (2 per layer url). --- lib/OpenLayers/Layer/Bing.js | 8 +-- lib/OpenLayers/Layer/Grid.js | 107 ++++++++++++++++++++++++++++------- tests/Layer/Grid.html | 33 +++++++++++ 3 files changed, 125 insertions(+), 23 deletions(-) diff --git a/lib/OpenLayers/Layer/Bing.js b/lib/OpenLayers/Layer/Bing.js index 638891dadb..06af556cf2 100644 --- a/lib/OpenLayers/Layer/Bing.js +++ b/lib/OpenLayers/Layer/Bing.js @@ -201,13 +201,13 @@ OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, { }, /** - * Method: drawTileFromQueue - * Draws the first tile from the tileQueue, and unqueues that tile + * Method: drawTilesFromQueue + * Draws tiles from the tileQueue, and unqueues the tiles */ - drawTileFromQueue: function() { + drawTilesFromQueue: function() { // don't start working on the queue before we have a url from initLayer if (this.url) { - OpenLayers.Layer.XYZ.prototype.drawTileFromQueue.apply(this, arguments); + OpenLayers.Layer.XYZ.prototype.drawTilesFromQueue.apply(this, arguments); } }, diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index 8791236cb6..28ae7734d5 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -144,7 +144,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { /** * Property: tileQueueId - * {Number} The id of the animation. + * {Number} The id of the animation. */ tileQueueId: null, @@ -240,6 +240,26 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * should not be zero. */ className: null, + + /** + * Property: tileCache + * {Object} Cached image elements, keyed by URL. + */ + tileCache: null, + + /** + * Property: tileCacheIndex + * {Array} URLs of cached tiles; first entry is least recently + * used. + */ + tileCacheIndex: null, + + /** + * APIProperty: tileCacheSize + * {Number} Number of image elements to keep referenced for fast reuse. + * Default is 128 per layer. + */ + tileCacheSize: 128, /** * Register a listener for a particular event with the following syntax: @@ -309,6 +329,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { arguments); this.grid = []; this.tileQueue = []; + this.tileCache = {}; + this.tileCacheIndex = []; this._removeBackBuffer = OpenLayers.Function.bind(this.removeBackBuffer, this); if (this.removeBackBufferDelay === null) { @@ -370,6 +392,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { this.grid = null; this.tileSize = null; + this.tileCache = null; OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this, arguments); }, @@ -585,37 +608,79 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { */ queueTileDraw: function(evt) { var tile = evt.object; - if (!~OpenLayers.Util.indexOf(this.tileQueue, tile)) { - // queue only if not in queue already - this.tileQueue.push(tile); + var queued = false; + if (this.async || !this.url || + !this.tileCache[this.getURL(tile.bounds)]) { + // queue only if not in tileCache already + if (!~OpenLayers.Util.indexOf(this.tileQueue, tile)) { + // add to queue only if not in queue already + this.tileQueue.push(tile); + } + queued = true; + if (!this.tileQueueId) { + this.tileQueueId = OpenLayers.Animation.start( + OpenLayers.Function.bind(this.drawTilesFromQueue, this), + null, this.div + ); + } } - if (!this.tileQueueId) { - this.tileQueueId = OpenLayers.Animation.start( - OpenLayers.Function.bind(this.drawTileFromQueue, this), - null, this.div - ); - } - return false; + return !queued; }, /** - * Method: drawTileFromQueue - * Draws the first tile from the tileQueue, and unqueues that tile + * Method: drawTilesFromQueue + * Draws tiles from the tileQueue, and unqueues the tiles */ - drawTileFromQueue: function() { - if (this.tileQueue.length === 0) { - this.clearTileQueue(); - } else { + drawTilesFromQueue: function() { + var numUrls = OpenLayers.Util.isArray(this.url) ? this.url.length : 1; + //TODO instead of using 2 * urls, we could keep track of the hosts used + // by all grid layers, and use a number that just saturates the number + // of parallel requests the browser can send + while (this.numLoadingTiles < 2 * numUrls) { + if (this.tileQueue.length === 0) { + this.clearTileQueue(); + break; + } this.tileQueue.shift().draw(true); } }, + /** + * Method: manageTileCache + * Adds, updates, removes and fetches cache entries. + * + * Parameters: + * evt - {Object} Listener argument of the tile's loadstart event + */ + manageTileCache: function(evt) { + var tile = evt.object; + if (this.tileCache[tile.url]) { + tile.imgDiv = this.tileCache[tile.url]; + OpenLayers.Util.removeItem(this.tileCacheIndex, tile.url); + this.tileCacheIndex.push(tile.url); + tile.positionTile(); + this.div.appendChild(tile.imgDiv); + } else { + tile.events.register('loadend', this, function loadend() { + tile.events.unregister('loadend', this, loadend); + if (!this.tileCache[tile.url]) { + if (this.tileCacheIndex.length >= this.tileCacheSize) { + delete this.tileCache[this.tileCacheIndex[0]]; + this.tileCacheIndex.shift(); + } + this.tileCache[tile.url] = tile.imgDiv; + this.tileCacheIndex.push(tile.url); + } + }); + } + }, + /** * Method: clearTileQueue * Clears the animation queue */ clearTileQueue: function() { - OpenLayers.Animation.stop(this.tileQueueId); + window.clearInterval(this.tileQueueId); this.tileQueueId = null; this.tileQueue = []; }, @@ -1102,7 +1167,11 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tile = new this.tileClass( this, position, bounds, null, this.tileSize, this.tileOptions ); - tile.events.register("beforedraw", this, this.queueTileDraw); + tile.events.on({ + beforedraw: this.queueTileDraw, + loadstart: this.manageTileCache, + scope: this + }); return tile; }, diff --git a/tests/Layer/Grid.html b/tests/Layer/Grid.html index 6e7d4181bf..ac1db09e89 100644 --- a/tests/Layer/Grid.html +++ b/tests/Layer/Grid.html @@ -100,6 +100,39 @@ map.destroy(); } + function test_manageTileCache(t) { + t.plan(9); + + var map = new OpenLayers.Map('map'); + layer = new OpenLayers.Layer.WMS(name, "../../img/blank.gif", params, { + tileCacheSize: 12 + }); + map.addLayer(layer); + map.setCenter([16, 48], 9); + + var firstInCache, sharedTile; + t.delay_call(2, function() { + t.eq(layer.tileCacheIndex.length, 12, "tiles cached"); + t.ok(~OpenLayers.Util.indexOf(layer.tileCacheIndex, layer.grid[1][2].url), "tile found in cache"); + t.ok(layer.tileCache[layer.grid[1][2].url] === layer.grid[1][2].imgDiv, "correct object cached"); + firstInCache = layer.tileCache[layer.tileCacheIndex[0]]; + sharedTile = layer.tileCache[layer.tileCacheIndex[11]]; + map.setCenter([17, 47]); + }); + t.delay_call(4, function() { + t.eq(layer.tileCacheIndex.length, 12, "tiles cached"); + t.ok(layer.tileCache[layer.grid[1][2].url] === layer.grid[1][2].imgDiv, "correct object cached"); + t.ok(!(firstInCache.getAttribute("src") in layer.tileCache), "old tile discarded"); + t.ok(sharedTile.getAttribute("src") in layer.tileCache, "shared tile still in cache"); + firstInCache = layer.tileCache[layer.tileCacheIndex[0]]; + map.setCenter([16, 48]); + }); + t.delay_call(6, function() { + t.ok(!(firstInCache.getAttribute("src") in layer.tileCache), "old tile discarded"); + t.ok(sharedTile.getAttribute("src") in layer.tileCache, "shared tile still in cache"); + }); + } + function test_queueTileDraw(t) { t.plan(3); OpenLayers.Layer.Grid.prototype.queueTileDraw = origQueueTileDraw; From 80fa25164976e86c8229b283b96acfbdbbf9d76a Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 29 Nov 2012 14:52:53 -0600 Subject: [PATCH 02/14] New TileManager This removes all tile queueing/loading specific code from Layer.Grid and creates a new class that manages tile loading and caching. --- examples/mobile-base.js | 3 +- examples/mobile-wmts-vienna.js | 3 +- lib/OpenLayers.js | 1 + lib/OpenLayers/Layer/Bing.js | 19 +- lib/OpenLayers/Layer/Grid.js | 192 +--------- lib/OpenLayers/Tile.js | 16 +- lib/OpenLayers/Tile/Image.js | 33 +- lib/OpenLayers/TileManager.js | 334 ++++++++++++++++++ tests/Layer/ArcGIS93Rest.html | 3 - tests/Layer/Bing.html | 3 +- tests/Layer/Grid.html | 86 +---- tests/Layer/WMS.html | 4 - tests/Tile.html | 2 +- tests/TileManager.html | 107 ++++++ tests/deprecated/Layer/MapServer/Untiled.html | 3 - tests/list-tests.html | 1 + 16 files changed, 499 insertions(+), 311 deletions(-) create mode 100644 lib/OpenLayers/TileManager.js create mode 100644 tests/TileManager.html diff --git a/examples/mobile-base.js b/examples/mobile-base.js index 5440f932e2..d47ae3fd88 100644 --- a/examples/mobile-base.js +++ b/examples/mobile-base.js @@ -3,7 +3,7 @@ var apiKey = "AqTGBsziZHIJYYxgivLBf0hVdrAk9mWO5cQcb8Yux8sW5M8c8opEC2lZqKR1ZZXf"; // initialize map when page ready -var map; +var map, tileManager; var gg = new OpenLayers.Projection("EPSG:4326"); var sm = new OpenLayers.Projection("EPSG:900913"); @@ -85,6 +85,7 @@ var init = function (onSelectFeatureFunction) { center: new OpenLayers.LonLat(0, 0), zoom: 1 }); + tileManager = new OpenLayers.TileManager({map: map}); var style = { fillOpacity: 0.1, diff --git a/examples/mobile-wmts-vienna.js b/examples/mobile-wmts-vienna.js index 05a97e3a66..f7b8a8116d 100644 --- a/examples/mobile-wmts-vienna.js +++ b/examples/mobile-wmts-vienna.js @@ -1,4 +1,4 @@ -var map; +var map, tileManager; (function() { // Set document language for css content @@ -124,6 +124,7 @@ var map; } } }); + tileManager = new OpenLayers.TileManager({map: map}); layerPanel.activateControl(mapButton); layerPanel.activateControl(labelButton); diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 4df00ddf62..f1344cbff9 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -395,6 +395,7 @@ "OpenLayers/Lang.js", "OpenLayers/Lang/en.js", "OpenLayers/Spherical.js", + "OpenLayers/TileManager.js", "OpenLayers/WPSClient.js", "OpenLayers/WPSProcess.js" ]; // etc. diff --git a/lib/OpenLayers/Layer/Bing.js b/lib/OpenLayers/Layer/Bing.js index 06af556cf2..06711b6a99 100644 --- a/lib/OpenLayers/Layer/Bing.js +++ b/lib/OpenLayers/Layer/Bing.js @@ -197,20 +197,12 @@ OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, { res.zoomMax + 1 - res.zoomMin, this.numZoomLevels ) }, true); + if (!this.isBaseLayer) { + this.redraw(); + } this.updateAttribution(); }, - - /** - * Method: drawTilesFromQueue - * Draws tiles from the tileQueue, and unqueues the tiles - */ - drawTilesFromQueue: function() { - // don't start working on the queue before we have a url from initLayer - if (this.url) { - OpenLayers.Layer.XYZ.prototype.drawTilesFromQueue.apply(this, arguments); - } - }, - + /** * Method: getURL * @@ -218,6 +210,9 @@ OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, { * bounds - {} */ getURL: function(bounds) { + if (!this.url) { + return; + } var xyz = this.getXYZ(bounds), x = xyz.x, y = xyz.y, z = xyz.z; var quadDigits = []; for (var i = z; i > 0; --i) { diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index 28ae7734d5..564789b6a3 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -113,14 +113,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { */ numLoadingTiles: 0, - /** - * APIProperty: tileLoadingDelay - * {Integer} Number of milliseconds before we shift and load - * tiles when panning. Ignored if is - * true. Default is 85. - */ - tileLoadingDelay: 85, - /** * Property: serverResolutions * {Array(Number}} This property is documented in subclasses as @@ -128,32 +120,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { */ serverResolutions: null, - /** - * Property: moveTimerId - * {Number} The id of the timer. - */ - moveTimerId: null, - - /** - * Property: deferMoveGriddedTiles - * {Function} A function that defers execution of by - * . If is true, this - * is null and unused. - */ - deferMoveGriddedTiles: null, - - /** - * Property: tileQueueId - * {Number} The id of the animation. - */ - tileQueueId: null, - - /** - * Property: tileQueue - * {Array()} Tiles queued for drawing. - */ - tileQueue: null, - /** * Property: loading * {Boolean} Indicates if tiles are being loaded. @@ -241,26 +207,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { */ className: null, - /** - * Property: tileCache - * {Object} Cached image elements, keyed by URL. - */ - tileCache: null, - - /** - * Property: tileCacheIndex - * {Array} URLs of cached tiles; first entry is least recently - * used. - */ - tileCacheIndex: null, - - /** - * APIProperty: tileCacheSize - * {Number} Number of image elements to keep referenced for fast reuse. - * Default is 128 per layer. - */ - tileCacheSize: 128, - /** * Register a listener for a particular event with the following syntax: * (code) @@ -275,6 +221,9 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * element - {DOMElement} A reference to layer.events.element. * * Supported event types: + * addtile - Triggered when a tile is added to this layer. Listeners receive + * an object as first argument, which has a tile property that + * references the tile that has been added. * tileloadstart - Triggered when a tile starts loading. Listeners receive * an object as first argument, which has a tile property that * references the tile that starts loading. @@ -289,6 +238,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * still hidden) if a tile failed to load. Listeners receive an object * as first argument, which has a tile property that references the * tile that could not be loaded. + * retile - Triggered when the layer recreates its tile grid. */ /** @@ -342,13 +292,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { 'olLayerGrid'; } - if (!OpenLayers.Animation.isNative) { - this.deferMoveGriddedTiles = OpenLayers.Function.bind(function() { - this.moveGriddedTiles(true); - this.moveTimerId = null; - }, this); - } - this.rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; }, @@ -375,7 +318,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { window.clearTimeout(this.moveTimerId); this.moveTimerId = null; } - this.clearTileQueue(); if(this.backBufferTimerId !== null) { window.clearTimeout(this.backBufferTimerId); this.backBufferTimerId = null; @@ -392,7 +334,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { this.grid = null; this.tileSize = null; - this.tileCache = null; OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this, arguments); }, @@ -402,7 +343,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * destroy() on each of them to kill circular references */ clearGrid:function() { - this.clearTileQueue(); if (this.grid) { for(var iRow=0, len=this.grid.length; iRow= this.tileCacheSize) { - delete this.tileCache[this.tileCacheIndex[0]]; - this.tileCacheIndex.shift(); - } - this.tileCache[tile.url] = tile.imgDiv; - this.tileCacheIndex.push(tile.url); - } - }); - } - }, - - /** - * Method: clearTileQueue - * Clears the animation queue - */ - clearTileQueue: function() { - window.clearInterval(this.tileQueueId); - this.tileQueueId = null; - this.tileQueue = []; - }, - /** * Method: destroyTile * @@ -843,10 +695,6 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { this.div.removeChild(this.backBuffer); this.backBuffer = null; this.backBufferResolution = null; - if(this.backBufferTimerId !== null) { - window.clearTimeout(this.backBufferTimerId); - this.backBufferTimerId = null; - } } }, @@ -914,7 +762,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * bounds - {} */ initSingleTile: function(bounds) { - this.clearTileQueue(); + this.events.triggerEvent("retile"); //determine new tile bounds var center = bounds.getCenterLonLat(); @@ -1047,7 +895,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * bounds - {} */ initGriddedTiles:function(bounds) { - this.clearTileQueue(); + this.events.triggerEvent("retile"); // work out mininum number of rows and columns; this is the number of // tiles required to cover the viewport plus at least one for panning @@ -1126,8 +974,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { //shave off exceess rows and colums this.removeExcessTiles(rowidx, colidx); - var resolution = this.getServerResolution(), - immediately = resolution === this.gridResolution; + var resolution = this.getServerResolution(); // store the resolution of the grid this.gridResolution = resolution; @@ -1136,7 +983,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { return a.distance - b.distance; }); for (var i=0, ii=tileData.length; i and return the result from . * * Parameters: - * immediately - {Boolean} When e.g. drawing was aborted by returning false - * from a *beforedraw* listener, the queue manager needs to pass true, - * so the tile will not be cleared and immediately be drawn. Otherwise, - * the tile will be cleared and a *beforedraw* event will be fired. + * force - {Boolean} No beforedraw event will be fired. * * Returns: - * {Boolean} Whether or not the tile should actually be drawn. + * {Boolean} Whether or not the tile should actually be drawn. Retruns null + * if a beforedraw listener returned false. */ - draw: function(immediately) { - if (!immediately) { + draw: function(force) { + if (!force) { //clear tile's contents and mark as not drawn this.clear(); } var draw = this.shouldDraw(); - if (draw && !immediately) { - draw = this.events.triggerEvent("beforedraw") !== false; + if (draw && !force && this.events.triggerEvent("beforedraw") === false) { + draw = null; } return draw; }, diff --git a/lib/OpenLayers/Tile/Image.js b/lib/OpenLayers/Tile/Image.js index 76107d09e8..608e81a4ef 100644 --- a/lib/OpenLayers/Tile/Image.js +++ b/lib/OpenLayers/Tile/Image.js @@ -146,11 +146,12 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { * Check that a tile should be drawn, and draw it. * * Returns: - * {Boolean} Was a tile drawn? + * {Boolean} Was a tile drawn? Or null if a beforedraw listener returned + * false. */ draw: function() { - var drawn = OpenLayers.Tile.prototype.draw.apply(this, arguments); - if (drawn) { + var shouldDraw = OpenLayers.Tile.prototype.draw.apply(this, arguments); + if (shouldDraw) { // The layer's reproject option is deprecated. if (this.layer != this.layer.map.baseLayer && this.layer.reproject) { // getBoundsFromBaseLayer is defined in deprecated.js. @@ -158,17 +159,17 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { } if (this.isLoading) { //if we're already loading, send 'reload' instead of 'loadstart'. - this._loadEvent = "reload"; + this._loadEvent = "reload"; } else { this.isLoading = true; this._loadEvent = "loadstart"; } this.positionTile(); this.renderTile(); - } else { + } else if (shouldDraw === false) { this.unload(); } - return drawn; + return shouldDraw; }, /** @@ -287,9 +288,11 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { this.events.triggerEvent(this._loadEvent); var img = this.getImage(); if (this.url && img.getAttribute("src") == this.url) { - this.onImageLoad(); + this._loadTimeout = window.setTimeout( + OpenLayers.Function.bind(this.onImageLoad, this), 0 + ); } else { - OpenLayers.Event.stopObservingElement(img); + this.stopLoading(); if (this.crossOriginKeyword) { img.removeAttribute("crossorigin"); } @@ -328,7 +331,7 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { } else { // Remove reference to the image, and leave it to the browser's // caching and garbage collection. - OpenLayers.Event.stopObservingElement(this.imgDiv); + this.stopLoading(); this.imgDiv = null; if (img.parentNode) { img.parentNode.removeChild(img); @@ -378,7 +381,7 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { */ onImageLoad: function() { var img = this.imgDiv; - OpenLayers.Event.stopObservingElement(img); + this.stopLoading(); img.style.visibility = 'inherit'; img.style.opacity = this.layer.opacity; this.isLoading = false; @@ -409,6 +412,16 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { } } }, + + /** + * Method: stopLoading + * Stops a loading sequence so won't be executed. + */ + stopLoading: function() { + OpenLayers.Event.stopObservingElement(this.imgDiv); + window.clearTimeout(this._loadTimeout); + delete this._loadTimeout; + }, /** * APIMethod: getCanvasContext diff --git a/lib/OpenLayers/TileManager.js b/lib/OpenLayers/TileManager.js new file mode 100644 index 0000000000..db035c6e07 --- /dev/null +++ b/lib/OpenLayers/TileManager.js @@ -0,0 +1,334 @@ +/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. */ + + +/** + * @requires OpenLayers/Layer/Grid.js + * @requires OpenLayers/Util.js + * @requires OpenLayers/BaseTypes.js + * @requires OpenLayers/BaseTypes/Element.js + */ + +/** + * Class: OpenLayers.TileManager + * Provides queueing of image requests and caching of image elements. + * + * Queueing avoids unnecessary image requests while changing zoom levels + * quickly, and helps improve dragging performance on mobile devices that show + * a lag in dragging when loading of new images start. and + * are the configuration options to control this behavior. + * + * Caching avoids setting the src on image elements for images that have already + * been used. A TileManager instance can have a private cache (when configured + * with a ), or share a cache with other instances, in which case the + * cache size can be controlled by adjusting . + */ +OpenLayers.TileManager = OpenLayers.Class({ + + /** + * APIProperty: map + * {} The map to manage tiles on. + */ + map: null, + + /** + * APIProperty: cacheSize + * {Number} Number of image elements to keep referenced in this instance's + * private cache for fast reuse. If not set, this instance will use the + * shared cache. To configure the shared cache size, set + * . + */ + cacheSize: null, + + /** + * APIProperty: moveDelay + * {Number} Delay in milliseconds after a map's move event before loading + * tiles. Default is 100. + */ + moveDelay: 100, + + /** + * APIProperty: zoomDelay + * {Number} Delay in milliseconds after a map's zoomend event before loading + * tiles. Default is 200. + */ + zoomDelay: 200, + + /** + * Property: tileQueueId + * {Number} The id of the animation. + */ + tileQueueId: null, + + /** + * Property: tileQueue + * {Array()} Tiles queued for drawing. + */ + tileQueue: null, + + /** + * Property: tileCache + * {Object} Cached image elements, keyed by URL. This is shared among all + * TileManager instances, unless is set on the instance. + */ + tileCache: {}, + + /** + * Property: tileCacheIndex + * {Array} URLs of cached tiles; first entry is least recently + * used. This is shared among all TileManager instances, unless + * is set on the instance. + */ + tileCacheIndex: [], + + /** + * Constructor: OpenLayers.TileManager + * Constructor for a new instance. + * + * Parameters: + * options - {Object} Configuration for this instance. + * + * Required options: + * map - {} The map to manage tiles on. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.tileQueue = []; + if (this.cacheSize == null) { + this.cacheSize = OpenLayers.TileManager.cacheSize; + } else { + this.tileCache = {}; + this.tileCacheIndex = []; + } + var map = this.map; + for (var i=0, ii=map.layers.length; i=0; --i) { + for (j=layer.grid[i].length-1; j>=0; --j) { + tile = layer.grid[i][j]; + this.addTile({tile: tile}); + if (tile.url) { + this.manageTileCache({object: tile}); + } + } + } + } + }, + + /** + * Method: addLayer + * Handles the map's removelayer event + * + * Parameters: + * evt - {Object} The listener argument + */ + removeLayer: function(evt) { + var layer = evt.layer; + if (layer instanceof OpenLayers.Layer.Grid) { + this.clearTileQueue({object: layer}); + layer.events.un({ + addtile: this.addTile, + retile: this.clearTileQueue, + scope: this + }); + } + }, + + /** + * Method: updateTimeout + * Applies the or to the loop. + * + * Parameters: + * delay - {Number} The delay to apply + */ + updateTimeout: function(delay) { + window.clearTimeout(this.tileQueueId); + if (this.tileQueue.length) { + this.tileQueueId = window.setTimeout( + OpenLayers.Function.bind(this.drawTilesFromQueue, this), + delay + ); + } + }, + + /** + * Method: addTile + * Listener for the layer's addtile event + * + * Parameters: + * evt - {Object} The listener argument + */ + addTile: function(evt) { + evt.tile.events.on({ + beforedraw: this.queueTileDraw, + loadstart: this.manageTileCache, + reload: this.manageTileCache, + unload: this.unloadTile, + scope: this + }); + }, + + /** + * Method: unloadTile + * Listener for the tile's unload event + * + * Parameters: + * evt - {Object} The listener argument + */ + unloadTile: function(evt) { + evt.object.events.un({ + beforedraw: this.queueTileDraw, + loadstart: this.manageTileCache, + reload: this.manageTileCache, + loadend: this.addToCache, + unload: this.unloadTile, + scope: this + }); + OpenLayers.Util.removeItem(this.tileQueue, evt.object); + }, + + /** + * Method: queueTileDraw + * Adds a tile to the queue that will draw it. + * + * Parameters: + * evt - {Object} Listener argument of the tile's beforedraw event + */ + queueTileDraw: function(evt) { + var tile = evt.object; + var queued = false; + var layer = tile.layer; + // queue only if image with same url not cached already + if (layer.url && (layer.async || + !this.tileCache[layer.getURL(tile.bounds)])) { + // add to queue only if not in queue already + if (!~OpenLayers.Util.indexOf(this.tileQueue, tile)) { + this.tileQueue.push(tile); + } + queued = true; + } + return !queued; + }, + + /** + * Method: drawTilesFromQueue + * Draws tiles from the tileQueue, and unqueues the tiles + */ + drawTilesFromQueue: function() { + while (this.tileQueue.length) { + this.tileQueue.shift().draw(true); + } + }, + + /** + * Method: manageTileCache + * Adds, updates, removes and fetches cache entries. + * + * Parameters: + * evt - {Object} Listener argument of the tile's loadstart event + */ + manageTileCache: function(evt) { + var tile = evt.object; + var img = this.tileCache[tile.url]; + // only use images from the cache that are not on a layer already + if (img && (!img.parentNode || + OpenLayers.Element.hasClass(img.parentNode, 'olBackBuffer'))) { + tile.imgDiv = img; + OpenLayers.Util.removeItem(this.tileCacheIndex, tile.url); + this.tileCacheIndex.push(tile.url); + tile.positionTile(); + tile.layer.div.appendChild(tile.imgDiv); + } else if (evt.type === 'loadstart') { + tile.events.register('loadend', this, this.addToCache); + } + }, + + /** + * Method: addToCache + * + * Parameters: + * evt - {Object} Listener argument for the tile's loadend event + */ + addToCache: function(evt) { + var tile = evt.object; + tile.events.unregister('loadend', this, this.addToCache); + if (!this.tileCache[tile.url]) { + if (!OpenLayers.Element.hasClass(tile.imgDiv, 'olImageLoadError')) { + if (this.tileCacheIndex.length >= this.cacheSize) { + delete this.tileCache[this.tileCacheIndex[0]]; + this.tileCacheIndex.shift(); + } + this.tileCache[tile.url] = tile.imgDiv; + this.tileCacheIndex.push(tile.url); + } + } + }, + + /** + * Method: clearTileQueue + * Clears the tile queue from tiles of a specific layer + * + * Parameters: + * evt - {Object} Listener argument of the layer's retile event + */ + clearTileQueue: function(evt) { + var layer = evt.object; + for (var i=this.tileQueue.length-1; i>=0; --i) { + if (this.tileQueue[i].layer === layer) { + this.tileQueue.splice(i, 1); + } + } + } + +}); + +/** + * APIProperty: OpenLayers.TileManager.cacheSize + * {Number} Number of image elements to keep referenced in the shared cache + * for fast reuse. Default is 512. + */ +OpenLayers.TileManager.cacheSize = 512; \ No newline at end of file diff --git a/tests/Layer/ArcGIS93Rest.html b/tests/Layer/ArcGIS93Rest.html index 6c00732e4e..568dff0326 100644 --- a/tests/Layer/ArcGIS93Rest.html +++ b/tests/Layer/ArcGIS93Rest.html @@ -4,9 +4,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/deprecated/Layer/MapServer/Untiled.html b/tests/deprecated/Layer/MapServer/Untiled.html index c3f9c44fe4..1b1dc94f36 100644 --- a/tests/deprecated/Layer/MapServer/Untiled.html +++ b/tests/deprecated/Layer/MapServer/Untiled.html @@ -5,9 +5,6 @@ From 273657a6ab4d25f4f0855354e9d416eff80c9978 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 30 Nov 2012 10:26:19 -0600 Subject: [PATCH 05/14] Adding API docs for the tileManager property --- lib/OpenLayers/Map.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/OpenLayers/Map.js b/lib/OpenLayers/Map.js index 8077d73c70..3e29852a9f 100644 --- a/lib/OpenLayers/Map.js +++ b/lib/OpenLayers/Map.js @@ -370,6 +370,14 @@ OpenLayers.Map = OpenLayers.Class({ * property at the time the control is added to the map. */ displayProjection: null, + + /** + * APIProperty: tileManager + * {} If configured at construction time, the map + * will use the TileManager to queue image requests and to cache tile image + * elements. + */ + tileManager: null, /** * APIProperty: fallThrough From ecbedf5536e17aa9b91a085ca48f215ac02c4af0 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 14 Dec 2012 09:19:06 +0100 Subject: [PATCH 06/14] Updating tests --- tests/Layer/Bing.html | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/tests/Layer/Bing.html b/tests/Layer/Bing.html index e43bfc82d3..89bbba7584 100644 --- a/tests/Layer/Bing.html +++ b/tests/Layer/Bing.html @@ -1,19 +1,5 @@ -