1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3 * full text of the license. */
7 * @requires OpenLayers/Tile.js
11 * Class: OpenLayers.Tile.Image
12 * Instances of OpenLayers.Tile.Image are used to manage the image tiles
13 * used by various layers. Create a new image tile with the
14 * <OpenLayers.Tile.Image> constructor.
19 OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, {
23 * {String} The URL of the image being requested. No default. Filled in by
24 * layer.getURL() function.
30 * {DOMElement} The div element which wraps the image.
36 * {DOMElement} The image element is appended to the frame. Any gutter on
37 * the image will be hidden behind the frame.
42 * Property: layerAlphaHack
43 * {Boolean} True if the png alpha hack needs to be applied on the layer's div.
48 * Property: isBackBuffer
49 * {Boolean} Is this tile a back buffer tile?
55 * {Float} Used in transition code only. This is the previous ratio
56 * of the back buffer tile resolution to the map resolution. Compared
57 * with the current ratio to determine if zooming occurred.
62 * Property: isFirstDraw
63 * {Boolean} Is this the first time the tile is being drawn?
64 * This is used to force resetBackBuffer to synchronize
65 * the backBufferTile with the foreground tile the first time
66 * the foreground tile loads so that if the user zooms
67 * before the layer has fully loaded, the backBufferTile for
68 * tiles that have been loaded can be used.
73 * Property: backBufferTile
74 * {<OpenLayers.Tile>} A clone of the tile used to create transition
75 * effects when the tile is moved or changes resolution.
79 /** TBD 3.0 - reorder the parameters to the init function to remove
80 * URL. the getUrl() function on the layer gets called on
81 * each draw(), so no need to specify it here.
83 * Constructor: OpenLayers.Tile.Image
84 * Constructor for a new <OpenLayers.Tile.Image> instance.
87 * layer - {<OpenLayers.Layer>} layer that the tile will go in.
88 * position - {<OpenLayers.Pixel>}
89 * bounds - {<OpenLayers.Bounds>}
90 * url - {<String>} Deprecated. Remove me in 3.0.
91 * size - {<OpenLayers.Size>}
93 initialize: function(layer, position, bounds, url, size) {
94 OpenLayers.Tile.prototype.initialize.apply(this, arguments);
96 this.url = url; //deprecated remove me
98 this.frame = document.createElement('div');
99 this.frame.style.overflow = 'hidden';
100 this.frame.style.position = 'absolute';
102 this.layerAlphaHack = this.layer.alpha && OpenLayers.Util.alphaHack();
107 * nullify references to prevent circular references and memory leaks
109 destroy: function() {
110 if (this.imgDiv != null) {
111 if (this.layerAlphaHack) {
112 // unregister the "load" handler
113 OpenLayers.Event.stopObservingElement(this.imgDiv.childNodes[0].id);
116 // unregister the "load" and "error" handlers. Only the "error" handler if
117 // this.layerAlphaHack is true.
118 OpenLayers.Event.stopObservingElement(this.imgDiv.id);
120 if (this.imgDiv.parentNode == this.frame) {
121 this.frame.removeChild(this.imgDiv);
122 this.imgDiv.map = null;
124 this.imgDiv.urls = null;
125 // abort any currently loading image
126 this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
129 if ((this.frame != null) && (this.frame.parentNode == this.layer.div)) {
130 this.layer.div.removeChild(this.frame);
134 /* clean up the backBufferTile if it exists */
135 if (this.backBufferTile) {
136 this.backBufferTile.destroy();
137 this.backBufferTile = null;
140 this.layer.events.unregister("loadend", this, this.resetBackBuffer);
142 OpenLayers.Tile.prototype.destroy.apply(this, arguments);
149 * obj - {<OpenLayers.Tile.Image>} The tile to be cloned
152 * {<OpenLayers.Tile.Image>} An exact clone of this <OpenLayers.Tile.Image>
154 clone: function (obj) {
156 obj = new OpenLayers.Tile.Image(this.layer,
163 //pick up properties from superclass
164 obj = OpenLayers.Tile.prototype.clone.apply(this, [obj]);
166 //dont want to directly copy the image div
175 * Check that a tile should be drawn, and draw it.
178 * {Boolean} Always returns true.
181 if (this.layer != this.layer.map.baseLayer && this.layer.reproject) {
182 this.bounds = this.getBoundsFromBaseLayer(this.position);
184 var drawTile = OpenLayers.Tile.prototype.draw.apply(this, arguments);
186 if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, this.layer.transitionEffect) != -1) {
188 //we use a clone of this tile to create a double buffer for visual
189 //continuity. The backBufferTile is used to create transition
190 //effects while the tile in the grid is repositioned and redrawn
191 if (!this.backBufferTile) {
192 this.backBufferTile = this.clone();
193 this.backBufferTile.hide();
194 // this is important. It allows the backBuffer to place itself
195 // appropriately in the DOM. The Image subclass needs to put
196 // the backBufferTile behind the main tile so the tiles can
197 // load over top and display as soon as they are loaded.
198 this.backBufferTile.isBackBuffer = true;
200 // potentially end any transition effects when the tile loads
201 this.events.register('loadend', this, this.resetBackBuffer);
203 // clear transition back buffer tile only after all tiles in
204 // this layer have loaded to avoid visual glitches
205 this.layer.events.register("loadend", this, this.resetBackBuffer);
207 // run any transition effects
208 this.startTransition();
210 // if we aren't going to draw the tile, then the backBuffer should
212 if (this.backBufferTile) {
213 this.backBufferTile.clear();
217 if (drawTile && this.isFirstDraw) {
218 this.events.register('loadend', this, this.showTile);
219 this.isFirstDraw = false;
227 if (this.isLoading) {
228 //if we're already loading, send 'reload' instead of 'loadstart'.
229 this.events.triggerEvent("reload");
231 this.isLoading = true;
232 this.events.triggerEvent("loadstart");
235 return this.renderTile();
239 * Method: resetBackBuffer
240 * Triggered by two different events, layer loadend, and tile loadend.
241 * In any of these cases, we check to see if we can hide the
242 * backBufferTile yet and update its parameters to match the
246 * - If the backBufferTile hasn't been drawn yet, reset it
247 * - If layer is still loading, show foreground tile but don't hide
248 * the backBufferTile yet
249 * - If layer is done loading, reset backBuffer tile and show
252 resetBackBuffer: function() {
254 if (this.backBufferTile &&
255 (this.isFirstDraw || !this.layer.numLoadingTiles)) {
256 this.isFirstDraw = false;
257 // check to see if the backBufferTile is within the max extents
258 // before rendering it
259 var maxExtent = this.layer.maxExtent;
260 var withinMaxExtent = (maxExtent &&
261 this.bounds.intersectsBounds(maxExtent, false));
262 if (withinMaxExtent) {
263 this.backBufferTile.position = this.position;
264 this.backBufferTile.bounds = this.bounds;
265 this.backBufferTile.size = this.size;
266 this.backBufferTile.imageSize = this.layer.imageSize || this.size;
267 this.backBufferTile.imageOffset = this.layer.imageOffset;
268 this.backBufferTile.resolution = this.layer.getResolution();
269 this.backBufferTile.renderTile();
272 this.backBufferTile.hide();
278 * Internal function to actually initialize the image tile,
279 * position it correctly, and set its url.
281 renderTile: function() {
282 if (this.imgDiv == null) {
286 this.imgDiv.viewRequestID = this.layer.map.viewRequestID;
288 if (this.layer.async) {
289 // Asyncronous image requests call the asynchronous getURL method
290 // on the layer to fetch an image that covers 'this.bounds', in the scope of
291 // 'this', setting the 'url' property of the layer itself, and running
292 // the callback 'positionFrame' when the image request returns.
293 this.layer.getURLasync(this.bounds, this, "url", this.positionImage);
295 // syncronous image requests get the url and position the frame immediately,
296 // and don't wait for an image request to come back.
298 // needed for changing to a different server for onload error
299 if (this.layer.url instanceof Array) {
300 this.imgDiv.urls = this.layer.url.slice();
303 this.url = this.layer.getURL(this.bounds);
305 // position the frame immediately
306 this.positionImage();
312 * Method: positionImage
313 * Using the properties currenty set on the layer, position the tile correctly.
314 * This method is used both by the async and non-async versions of the Tile.Image
317 positionImage: function() {
318 // if the this layer doesn't exist at the point the image is
319 // returned, do not attempt to use it for size computation
320 if ( this.layer == null )
323 // position the frame
324 OpenLayers.Util.modifyDOMElement(this.frame,
325 null, this.position, this.size);
327 var imageSize = this.layer.getImageSize();
328 if (this.layerAlphaHack) {
329 OpenLayers.Util.modifyAlphaImageDiv(this.imgDiv,
330 null, null, imageSize, this.url);
332 OpenLayers.Util.modifyDOMElement(this.imgDiv,
333 null, null, imageSize) ;
334 this.imgDiv.src = this.url;
340 * Clear the tile of any bounds/position-related data so that it can
341 * be reused in a new location.
346 if (OpenLayers.Tile.Image.useBlankTile) {
347 this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
354 * Creates the imgDiv property on the tile.
356 initImgDiv: function() {
358 var offset = this.layer.imageOffset;
359 var size = this.layer.getImageSize();
361 if (this.layerAlphaHack) {
362 this.imgDiv = OpenLayers.Util.createAlphaImageDiv(null,
372 this.imgDiv = OpenLayers.Util.createImage(null,
382 this.imgDiv.className = 'olTileImage';
384 /* checkImgURL used to be used to called as a work around, but it
385 ended up hiding problems instead of solving them and broke things
386 like relative URLs. See discussion on the dev list:
387 http://openlayers.org/pipermail/dev/2007-January/000205.html
389 OpenLayers.Event.observe( this.imgDiv, "load",
390 OpenLayers.Function.bind(this.checkImgURL, this) );
392 this.frame.style.zIndex = this.isBackBuffer ? 0 : 1;
393 this.frame.appendChild(this.imgDiv);
394 this.layer.div.appendChild(this.frame);
396 if(this.layer.opacity != null) {
398 OpenLayers.Util.modifyDOMElement(this.imgDiv, null, null, null,
403 // we need this reference to check back the viewRequestID
404 this.imgDiv.map = this.layer.map;
406 //bind a listener to the onload of the image div so that we
407 // can register when a tile has finished loading.
408 var onload = function() {
410 //normally isLoading should always be true here but there are some
411 // right funky conditions where loading and then reloading a tile
412 // with the same url *really*fast*. this check prevents sending
413 // a 'loadend' if the msg has already been sent
415 if (this.isLoading) {
416 this.isLoading = false;
417 this.events.triggerEvent("loadend");
421 if (this.layerAlphaHack) {
422 OpenLayers.Event.observe(this.imgDiv.childNodes[0], 'load',
423 OpenLayers.Function.bind(onload, this));
425 OpenLayers.Event.observe(this.imgDiv, 'load',
426 OpenLayers.Function.bind(onload, this));
430 // Bind a listener to the onerror of the image div so that we
431 // can registere when a tile has finished loading with errors.
432 var onerror = function() {
434 // If we have gone through all image reload attempts, it is time
435 // to realize that we are done with this image. Since
436 // OpenLayers.Util.onImageLoadError already has taken care about
437 // the error, we can continue as if the image was loaded
439 if (this.imgDiv._attempts > OpenLayers.IMAGE_RELOAD_ATTEMPTS) {
443 OpenLayers.Event.observe(this.imgDiv, "error",
444 OpenLayers.Function.bind(onerror, this));
448 * Method: checkImgURL
449 * Make sure that the image that just loaded is the one this tile is meant
450 * to display, since panning/zooming might have changed the tile's URL in
451 * the meantime. If the tile URL did change before the image loaded, set
452 * the imgDiv display to 'none', as either (a) it will be reset to visible
453 * when the new URL loads in the image, or (b) we don't want to display
454 * this tile after all because its new bounds are outside our maxExtent.
456 * This function should no longer be neccesary with the improvements to
457 * Grid.js in OpenLayers 2.3. The lack of a good isEquivilantURL function
458 * caused problems in 2.2, but it's possible that with the improved
459 * isEquivilant URL function, this might be neccesary at some point.
461 * See discussion in the thread at
462 * http://openlayers.org/pipermail/dev/2007-January/000205.html
464 checkImgURL: function () {
465 // Sometimes our image will load after it has already been removed
466 // from the map, in which case this check is not needed.
468 var loaded = this.layerAlphaHack ? this.imgDiv.firstChild.src : this.imgDiv.src;
469 if (!OpenLayers.Util.isEquivalentUrl(loaded, this.url)) {
476 * Method: startTransition
477 * This method is invoked on tiles that are backBuffers for tiles in the
478 * grid. The grid tile is about to be cleared and a new tile source
479 * loaded. This is where the transition effect needs to be started
480 * to provide visual continuity.
482 startTransition: function() {
483 // backBufferTile has to be valid and ready to use
484 if (!this.backBufferTile || !this.backBufferTile.imgDiv) {
488 // calculate the ratio of change between the current resolution of the
489 // backBufferTile and the layer. If several animations happen in a
490 // row, then the backBufferTile will scale itself appropriately for
493 if (this.backBufferTile.resolution) {
494 ratio = this.backBufferTile.resolution / this.layer.getResolution();
497 // if the ratio is not the same as it was last time (i.e. we are
498 // zooming), then we need to adjust the backBuffer tile
499 if (ratio != this.lastRatio) {
500 if (this.layer.transitionEffect == 'resize') {
501 // In this case, we can just immediately resize the
503 var upperLeft = new OpenLayers.LonLat(
504 this.backBufferTile.bounds.left,
505 this.backBufferTile.bounds.top
507 var size = new OpenLayers.Size(
508 this.backBufferTile.size.w * ratio,
509 this.backBufferTile.size.h * ratio
512 var px = this.layer.map.getLayerPxFromLonLat(upperLeft);
513 OpenLayers.Util.modifyDOMElement(this.backBufferTile.frame,
515 var imageSize = this.backBufferTile.imageSize;
516 imageSize = new OpenLayers.Size(imageSize.w * ratio,
517 imageSize.h * ratio);
518 var imageOffset = this.backBufferTile.imageOffset;
520 imageOffset = new OpenLayers.Pixel(
521 imageOffset.x * ratio, imageOffset.y * ratio
525 OpenLayers.Util.modifyDOMElement(
526 this.backBufferTile.imgDiv, null, imageOffset, imageSize
529 this.backBufferTile.show();
532 // default effect is just to leave the existing tile
533 // until the new one loads if this is a singleTile and
534 // there was no change in resolution. Otherwise we
535 // don't bother to show the backBufferTile at all
536 if (this.layer.singleTile) {
537 this.backBufferTile.show();
539 this.backBufferTile.hide();
542 this.lastRatio = ratio;
548 * Show the tile by showing its frame.
551 this.frame.style.display = '';
552 // Force a reflow on gecko based browsers to actually show the element
553 // before continuing execution.
554 if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS,
555 this.layer.transitionEffect) != -1) {
556 if (navigator.userAgent.toLowerCase().indexOf("gecko") != -1) {
557 this.frame.scrollLeft = this.frame.scrollLeft;
564 * Hide the tile by hiding its frame.
567 this.frame.style.display = 'none';
570 CLASS_NAME: "OpenLayers.Tile.Image"
574 OpenLayers.Tile.Image.useBlankTile = (
575 OpenLayers.Util.getBrowserName() == "safari" ||
576 OpenLayers.Util.getBrowserName() == "opera");