--- a/labs/openlayers/lib/OpenLayers/Tile/Image.js +++ b/labs/openlayers/lib/OpenLayers/Tile/Image.js @@ -1,1 +1,579 @@ - +/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + + +/** + * @requires OpenLayers/Tile.js + */ + +/** + * Class: OpenLayers.Tile.Image + * Instances of OpenLayers.Tile.Image are used to manage the image tiles + * used by various layers. Create a new image tile with the + * constructor. + * + * Inherits from: + * - + */ +OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { + + /** + * Property: url + * {String} The URL of the image being requested. No default. Filled in by + * layer.getURL() function. + */ + url: null, + + /** + * Property: imgDiv + * {DOMElement} The div element which wraps the image. + */ + imgDiv: null, + + /** + * Property: frame + * {DOMElement} The image element is appended to the frame. Any gutter on + * the image will be hidden behind the frame. + */ + frame: null, + + /** + * Property: layerAlphaHack + * {Boolean} True if the png alpha hack needs to be applied on the layer's div. + */ + layerAlphaHack: null, + + /** + * 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 - reorder the parameters to the init function to remove + * URL. the getUrl() function on the layer gets called on + * each draw(), so no need to specify it here. + * + * Constructor: OpenLayers.Tile.Image + * Constructor for a new instance. + * + * Parameters: + * layer - {} layer that the tile will go in. + * position - {} + * bounds - {} + * url - {} Deprecated. Remove me in 3.0. + * size - {} + */ + initialize: function(layer, position, bounds, url, size) { + OpenLayers.Tile.prototype.initialize.apply(this, arguments); + + this.url = url; //deprecated remove me + + this.frame = document.createElement('div'); + this.frame.style.overflow = 'hidden'; + this.frame.style.position = 'absolute'; + + this.layerAlphaHack = this.layer.alpha && OpenLayers.Util.alphaHack(); + }, + + /** + * APIMethod: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + if (this.imgDiv != null) { + if (this.layerAlphaHack) { + // unregister the "load" handler + OpenLayers.Event.stopObservingElement(this.imgDiv.childNodes[0]); + } + + // unregister the "load" and "error" handlers. Only the "error" handler if + // this.layerAlphaHack is true. + OpenLayers.Event.stopObservingElement(this.imgDiv); + + if (this.imgDiv.parentNode == this.frame) { + this.frame.removeChild(this.imgDiv); + this.imgDiv.map = null; + } + this.imgDiv.urls = null; + // abort any currently loading image + this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif"; + } + this.imgDiv = null; + if ((this.frame != null) && (this.frame.parentNode == this.layer.div)) { + this.layer.div.removeChild(this.frame); + } + this.frame = null; + + /* clean up the backBufferTile if it exists */ + if (this.backBufferTile) { + this.backBufferTile.destroy(); + this.backBufferTile = null; + } + + this.layer.events.unregister("loadend", this, this.resetBackBuffer); + + OpenLayers.Tile.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: clone + * + * Parameters: + * obj - {} The tile to be cloned + * + * Returns: + * {} An exact clone of this + */ + clone: function (obj) { + if (obj == null) { + obj = new OpenLayers.Tile.Image(this.layer, + this.position, + this.bounds, + this.url, + this.size); + } + + //pick up properties from superclass + obj = OpenLayers.Tile.prototype.clone.apply(this, [obj]); + + //dont want to directly copy the image div + obj.imgDiv = null; + + + return obj; + }, + + /** + * Method: draw + * Check that a tile should be drawn, and draw it. + * + * Returns: + * {Boolean} Always returns true. + */ + draw: function() { + if (this.layer != this.layer.map.baseLayer && this.layer.reproject) { + this.bounds = this.getBoundsFromBaseLayer(this.position); + } + var drawTile = OpenLayers.Tile.prototype.draw.apply(this, arguments); + + if ((OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, this.layer.transitionEffect) != -1) || + this.layer.singleTile) { + 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; + } + } + + if (!drawTile) { + return false; + } + + if (this.isLoading) { + //if we're already loading, send 'reload' instead of 'loadstart'. + this.events.triggerEvent("reload"); + } else { + this.isLoading = true; + this.events.triggerEvent("loadstart"); + } + + return this.renderTile(); + }, + + /** + * 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.getImageSize(this.bounds) || this.size; + this.backBufferTile.imageOffset = this.layer.imageOffset; + this.backBufferTile.resolution = this.layer.getResolution(); + this.backBufferTile.renderTile(); + } + + this.backBufferTile.hide(); + } + }, + + /** + * 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(); + } + + this.imgDiv.viewRequestID = this.layer.map.viewRequestID; + + if (this.layer.async) { + // Asyncronous image requests call the asynchronous getURL method + // on the layer to fetch an image that covers 'this.bounds', in the scope of + // 'this', setting the 'url' property of the layer itself, and running + // the callback 'positionFrame' when the image request returns. + this.layer.getURLasync(this.bounds, this, "url", this.positionImage); + } else { + // syncronous image requests get the url and position the frame immediately, + // and don't wait for an image request to come back. + + // needed for changing to a different server for onload error + if (this.layer.url instanceof Array) { + this.imgDiv.urls = this.layer.url.slice(); + } + + this.url = this.layer.getURL(this.bounds); + + // position the frame immediately + this.positionImage(); + } + return true; + }, + + /** + * Method: positionImage + * Using the properties currenty set on the layer, position the tile correctly. + * This method is used both by the async and non-async versions of the Tile.Image + * code. + */ + positionImage: function() { + // if the this layer doesn't exist at the point the image is + // returned, do not attempt to use it for size computation + if (this.layer === null) { + return; + } + // position the frame + OpenLayers.Util.modifyDOMElement(this.frame, + null, this.position, this.size); + + var imageSize = this.layer.getImageSize(this.bounds); + if (this.layerAlphaHack) { + OpenLayers.Util.modifyAlphaImageDiv(this.imgDiv, + null, null, imageSize, this.url); + } else { + OpenLayers.Util.modifyDOMElement(this.imgDiv, + null, null, imageSize) ; + this.imgDiv.src = this.url; + } + }, + + /** + * Method: clear + * Clear the tile of any bounds/position-related data so that it can + * be reused in a new location. + */ + clear: function() { + if(this.imgDiv) { + this.hide(); + if (OpenLayers.Tile.Image.useBlankTile) { + this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif"; + } + } + }, + + /** + * Method: initImgDiv + * Creates the imgDiv property on the tile. + */ + initImgDiv: function() { + + var offset = this.layer.imageOffset; + var size = this.layer.getImageSize(this.bounds); + + if (this.layerAlphaHack) { + this.imgDiv = OpenLayers.Util.createAlphaImageDiv(null, + offset, + size, + null, + "relative", + null, + null, + null, + true); + } else { + this.imgDiv = OpenLayers.Util.createImage(null, + offset, + size, + null, + "relative", + null, + null, + true); + } + + this.imgDiv.className = 'olTileImage'; + + /* checkImgURL used to be used to called as a work around, but it + ended up hiding problems instead of solving them and broke things + like relative URLs. See discussion on the dev list: + http://openlayers.org/pipermail/dev/2007-January/000205.html + + 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); + + if(this.layer.opacity != null) { + + OpenLayers.Util.modifyDOMElement(this.imgDiv, null, null, null, + null, null, null, + this.layer.opacity); + } + + // we need this reference to check back the viewRequestID + this.imgDiv.map = this.layer.map; + + //bind a listener to the onload of the image div so that we + // can register when a tile has finished loading. + var onload = function() { + + //normally isLoading should always be true here but there are some + // right funky conditions where loading and then reloading a tile + // with the same url *really*fast*. this check prevents sending + // a 'loadend' if the msg has already been sent + // + if (this.isLoading) { + this.isLoading = false; + this.events.triggerEvent("loadend"); + } + }; + + if (this.layerAlphaHack) { + OpenLayers.Event.observe(this.imgDiv.childNodes[0], 'load', + OpenLayers.Function.bind(onload, this)); + } else { + OpenLayers.Event.observe(this.imgDiv, 'load', + OpenLayers.Function.bind(onload, this)); + } + + + // Bind a listener to the onerror of the image div so that we + // can registere when a tile has finished loading with errors. + var onerror = function() { + + // If we have gone through all image reload attempts, it is time + // to realize that we are done with this image. Since + // OpenLayers.Util.onImageLoadError already has taken care about + // the error, we can continue as if the image was loaded + // successfully. + if (this.imgDiv._attempts > OpenLayers.IMAGE_RELOAD_ATTEMPTS) { + onload.call(this); + } + }; + OpenLayers.Event.observe(this.imgDiv, "error", + OpenLayers.Function.bind(onerror, this)); + }, + + /** + * Method: checkImgURL + * Make sure that the image that just loaded is the one this tile is meant + * to display, since panning/zooming might have changed the tile's URL in + * the meantime. If the tile URL did change before the image loaded, set + * the imgDiv display to 'none', as either (a) it will be reset to visible + * when the new URL loads in the image, or (b) we don't want to display + * this tile after all because its new bounds are outside our maxExtent. + * + * This function should no longer be neccesary with the improvements to + * Grid.js in OpenLayers 2.3. The lack of a good isEquivilantURL function + * caused problems in 2.2, but it's possible that with the improved + * isEquivilant URL function, this might be neccesary at some point. + * + * See discussion in the thread at + * http://openlayers.org/pipermail/dev/2007-January/000205.html + */ + checkImgURL: function () { + // Sometimes our image will load after it has already been removed + // from the map, in which case this check is not needed. + if (this.layer) { + var loaded = this.layerAlphaHack ? this.imgDiv.firstChild.src : this.imgDiv.src; + if (!OpenLayers.Util.isEquivalentUrl(loaded, this.url)) { + 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 (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, + this.layer.transitionEffect) != -1) { + 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" + } +); + +OpenLayers.Tile.Image.useBlankTile = ( + OpenLayers.Util.getBrowserName() == "safari" || + OpenLayers.Util.getBrowserName() == "opera"); +