From 3fa5487c375e10d1411446b786176dd6308536ec Mon Sep 17 00:00:00 2001 From: Paul Spencer Date: Thu, 6 Mar 2008 22:50:44 +0000 Subject: [PATCH] Re #933. Apply transition effect patch to trunk, many thanks to Erik, Tim and Chris for support. r=crschmidt, tschaub. git-svn-id: http://svn.openlayers.org/trunk/openlayers@6452 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf --- examples/transition.html | 69 +++++++++++++++ lib/OpenLayers/Layer.js | 20 +++++ lib/OpenLayers/Tile.js | 161 +++++++++++++++++++++++++++++++++-- lib/OpenLayers/Tile/Image.js | 108 ++++++++++++++++++++++- 4 files changed, 349 insertions(+), 9 deletions(-) create mode 100644 examples/transition.html diff --git a/examples/transition.html b/examples/transition.html new file mode 100644 index 0000000000..db34e14bef --- /dev/null +++ b/examples/transition.html @@ -0,0 +1,69 @@ + + + OpenLayers Transitions Example + + + + + +

Transition Example

+

+ Demonstrates the use of transition effects in tiled and untiled layers. +

+
+
+ There are two transitions that are currently implemented: null (the + default) and 'resize'. The default transition effect is used when no + transition is specified and is implemented as no transition effect except + for panning singleTile layers. The 'resize' effect resamples the current + tile and displays it stretched or compressed until the new tile is available. + +
+ + + \ No newline at end of file diff --git a/lib/OpenLayers/Layer.js b/lib/OpenLayers/Layer.js index a8ec7317ca..78bc44cee8 100644 --- a/lib/OpenLayers/Layer.js +++ b/lib/OpenLayers/Layer.js @@ -257,6 +257,26 @@ OpenLayers.Layer = OpenLayers.Class({ */ wrapDateLine: false, + /** + * APIProperty: transitionEffect + * {String} The transition effect to use when the map is panned or + * zoomed. + * + * There are currently two supported values: + * - *null* No transition effect (the default). + * - *resize* Existing tiles are resized on zoom to provide a visual + * effect of the zoom having taken place immediately. As the + * new tiles become available, they are drawn over top of the + * resized tiles. + */ + transitionEffect: null, + + /** + * Property: SUPPORTED_TRANSITIONS + * {Array} An immutable (that means don't change it!) list of supported + * transitionEffect values. + */ + SUPPORTED_TRANSITIONS: ['resize'], /** * Constructor: OpenLayers.Layer diff --git a/lib/OpenLayers/Tile.js b/lib/OpenLayers/Tile.js index 3c7834d3b0..06dda8896a 100644 --- a/lib/OpenLayers/Tile.js +++ b/lib/OpenLayers/Tile.js @@ -80,6 +80,38 @@ OpenLayers.Tile = OpenLayers.Class({ */ isLoading: false, + /** + * Property: isBackBuffer + * {Boolean} Is this tile a back buffer tile? + */ + isBackBuffer: false, + + /** + * Property: lastRatio + * {Float} Used in transition code only. This is the previous ratio + * of the back buffer tile resolution to the map resolution. Compared + * with the current ratio to determine if zooming occurred. + */ + lastRatio: 1, + + /** + * Property: isFirstDraw + * {Boolean} Is this the first time the tile is being drawn? + * This is used to force resetBackBuffer to synchronize + * the backBufferTile with the foreground tile the first time + * the foreground tile loads so that if the user zooms + * before the layer has fully loaded, the backBufferTile for + * tiles that have been loaded can be used. + */ + isFirstDraw: true, + + /** + * Property: backBufferTile + * {} A clone of the tile used to create transition + * effects when the tile is moved or changes resolution. + */ + backBufferTile: null, + /** TBD 3.0 -- remove 'url' from the list of parameters to the constructor. * there is no need for the base tile class to have a url. * @@ -111,6 +143,13 @@ OpenLayers.Tile = OpenLayers.Class({ * Nullify references to prevent circular references and memory leaks. */ destroy:function() { + if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, + this.layer.transitionEffect) != -1) { + this.layer.events.unregister("loadend", this, this.resetBackBuffer); + this.events.unregister('loadend', this, this.resetBackBuffer); + } else { + this.events.unregister('loadend', this, this.showTile); + } this.layer = null; this.bounds = null; this.size = null; @@ -118,6 +157,12 @@ OpenLayers.Tile = OpenLayers.Class({ this.events.destroy(); this.events = null; + + /* clean up the backBufferTile if it exists */ + if (this.backBufferTile) { + this.backBufferTile.destroy(); + this.backBufferTile = null; + } }, /** @@ -156,17 +201,56 @@ OpenLayers.Tile = OpenLayers.Class({ * depend on the return to know if they should draw or not. */ draw: function() { - - //clear tile's contents and mark as not drawn - this.clear(); - var maxExtent = this.layer.maxExtent; var withinMaxExtent = (maxExtent && this.bounds.intersectsBounds(maxExtent, false)); // The only case where we *wouldn't* want to draw the tile is if the // tile is outside its layer's maxExtent. - return (withinMaxExtent || this.layer.displayOutsideMaxExtent); + var drawTile = (withinMaxExtent || this.layer.displayOutsideMaxExtent); + + if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, this.layer.transitionEffect) != -1) { + if (drawTile) { + //we use a clone of this tile to create a double buffer for visual + //continuity. The backBufferTile is used to create transition + //effects while the tile in the grid is repositioned and redrawn + if (!this.backBufferTile) { + this.backBufferTile = this.clone(); + this.backBufferTile.hide(); + // this is important. It allows the backBuffer to place itself + // appropriately in the DOM. The Image subclass needs to put + // the backBufferTile behind the main tile so the tiles can + // load over top and display as soon as they are loaded. + this.backBufferTile.isBackBuffer = true; + + // potentially end any transition effects when the tile loads + this.events.register('loadend', this, this.resetBackBuffer); + + // clear transition back buffer tile only after all tiles in + // this layer have loaded to avoid visual glitches + this.layer.events.register("loadend", this, this.resetBackBuffer); + } + // run any transition effects + this.startTransition(); + } else { + // if we aren't going to draw the tile, then the backBuffer should + // be hidden too! + if (this.backBufferTile) { + this.backBufferTile.clear(); + } + } + } else { + if (drawTile && this.isFirstDraw) { + this.events.register('loadend', this, this.showTile); + this.isFirstDraw = false; + } + } + this.shouldDraw = drawTile; + + //clear tile's contents and mark as not drawn + this.clear(); + + return drawTile; }, /** @@ -237,6 +321,71 @@ OpenLayers.Tile = OpenLayers.Class({ topLeft.lat); return bounds; }, - + + /** + * Method: startTransition + * Prepare the tile for a transition effect. To be + * implemented by subclasses. + */ + startTransition: function() {}, + + /** + * Method: resetBackBuffer + * Triggered by two different events, layer loadend, and tile loadend. + * In any of these cases, we check to see if we can hide the + * backBufferTile yet and update its parameters to match the + * foreground tile. + * + * Basic logic: + * - If the backBufferTile hasn't been drawn yet, reset it + * - If layer is still loading, show foreground tile but don't hide + * the backBufferTile yet + * - If layer is done loading, reset backBuffer tile and show + * foreground tile + */ + resetBackBuffer: function() { + this.showTile(); + if (this.backBufferTile && + (this.isFirstDraw || !this.layer.numLoadingTiles)) { + this.isFirstDraw = false; + // check to see if the backBufferTile is within the max extents + // before rendering it + var maxExtent = this.layer.maxExtent; + var withinMaxExtent = (maxExtent && + this.bounds.intersectsBounds(maxExtent, false)); + if (withinMaxExtent) { + this.backBufferTile.position = this.position; + this.backBufferTile.bounds = this.bounds; + this.backBufferTile.size = this.size; + this.backBufferTile.imageSize = this.layer.imageSize || this.size; + this.backBufferTile.imageOffset = this.layer.imageOffset; + this.backBufferTile.resolution = this.layer.getResolution(); + this.backBufferTile.renderTile(); + } + } + }, + + /** + * Method: showTile + * Show the tile only if it should be drawn. + */ + showTile: function() { + if (this.shouldDraw) { + this.show(); + } + }, + + /** + * Method: show + * Show the tile. To be implemented by subclasses. + */ + show: function() { }, + + /** + * Method: hide + * Hide the tile. To be implemented by subclasses. + */ + hide: function() { }, + CLASS_NAME: "OpenLayers.Tile" }); diff --git a/lib/OpenLayers/Tile/Image.js b/lib/OpenLayers/Tile/Image.js index ccce66ba71..0968de382e 100644 --- a/lib/OpenLayers/Tile/Image.js +++ b/lib/OpenLayers/Tile/Image.js @@ -146,6 +146,15 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { this.events.triggerEvent("loadstart"); } + return this.renderTile(); + }, + + /** + * Method: renderTile + * Internal function to actually initialize the image tile, + * position it correctly, and set its url. + */ + renderTile: function() { if (this.imgDiv == null) { this.initImgDiv(); } @@ -162,9 +171,9 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { OpenLayers.Util.modifyAlphaImageDiv(this.imgDiv, null, null, imageSize, this.url); } else { - this.imgDiv.src = this.url; OpenLayers.Util.modifyDOMElement(this.imgDiv, null, null, imageSize) ; + this.imgDiv.src = this.url; } return true; }, @@ -176,7 +185,7 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { */ clear: function() { if(this.imgDiv) { - this.imgDiv.style.display = "none"; + this.hide(); if (OpenLayers.Tile.Image.useBlankTile) { this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif"; } @@ -223,6 +232,7 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { OpenLayers.Event.observe( this.imgDiv, "load", OpenLayers.Function.bind(this.checkImgURL, this) ); */ + this.frame.style.zIndex = this.isBackBuffer ? 0 : 1; this.frame.appendChild(this.imgDiv); this.layer.div.appendChild(this.frame); @@ -300,11 +310,103 @@ OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { if (this.layer) { var loaded = this.layerAlphaHack ? this.imgDiv.firstChild.src : this.imgDiv.src; if (!OpenLayers.Util.isEquivalentUrl(loaded, this.url)) { - this.imgDiv.style.display = "none"; + this.hide(); } } }, + + /** + * Method: startTransition + * This method is invoked on tiles that are backBuffers for tiles in the + * grid. The grid tile is about to be cleared and a new tile source + * loaded. This is where the transition effect needs to be started + * to provide visual continuity. + */ + startTransition: function() { + // backBufferTile has to be valid and ready to use + if (!this.backBufferTile || !this.backBufferTile.imgDiv) { + return; + } + // calculate the ratio of change between the current resolution of the + // backBufferTile and the layer. If several animations happen in a + // row, then the backBufferTile will scale itself appropriately for + // each request. + var ratio = 1; + if (this.backBufferTile.resolution) { + ratio = this.backBufferTile.resolution / this.layer.getResolution(); + } + + // if the ratio is not the same as it was last time (i.e. we are + // zooming), then we need to adjust the backBuffer tile + if (ratio != this.lastRatio) { + if (this.layer.transitionEffect == 'resize') { + // In this case, we can just immediately resize the + // backBufferTile. + var upperLeft = new OpenLayers.LonLat( + this.backBufferTile.bounds.left, + this.backBufferTile.bounds.top + ); + var size = new OpenLayers.Size( + this.backBufferTile.size.w * ratio, + this.backBufferTile.size.h * ratio + ); + + var px = this.layer.map.getLayerPxFromLonLat(upperLeft); + OpenLayers.Util.modifyDOMElement(this.backBufferTile.frame, + null, px, size); + var imageSize = this.backBufferTile.imageSize; + imageSize = new OpenLayers.Size(imageSize.w * ratio, + imageSize.h * ratio); + var imageOffset = this.backBufferTile.imageOffset; + if(imageOffset) { + imageOffset = new OpenLayers.Pixel( + imageOffset.x * ratio, imageOffset.y * ratio + ); + } + + OpenLayers.Util.modifyDOMElement( + this.backBufferTile.imgDiv, null, imageOffset, imageSize + ) ; + + this.backBufferTile.show(); + } + } else { + // default effect is just to leave the existing tile + // until the new one loads if this is a singleTile and + // there was no change in resolution. Otherwise we + // don't bother to show the backBufferTile at all + if (this.layer.singleTile) { + this.backBufferTile.show(); + } else { + this.backBufferTile.hide(); + } + } + this.lastRatio = ratio; + + }, + + /** + * Method: show + * Show the tile by showing its frame. + */ + show: function() { + this.frame.style.display = ''; + // Force a reflow on gecko based browsers to actually show the element + // before continuing execution. + if (navigator.userAgent.toLowerCase().indexOf("gecko") != -1) { + this.frame.scrollLeft = this.frame.scrollLeft; + } + }, + + /** + * Method: hide + * Hide the tile by hiding its frame. + */ + hide: function() { + this.frame.style.display = 'none'; + }, + CLASS_NAME: "OpenLayers.Tile.Image" } );