From 80fa25164976e86c8229b283b96acfbdbbf9d76a Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 29 Nov 2012 14:52:53 -0600 Subject: [PATCH] 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 @@