]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Tile/Image.js
initial commit
[syp.git] / openlayers / lib / OpenLayers / Tile / Image.js
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. */
4
5
6 /**
7  * @requires OpenLayers/Tile.js
8  */
9
10 /**
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.
15  *
16  * Inherits from:
17  *  - <OpenLayers.Tile>
18  */
19 OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, {
20
21     /** 
22      * Property: url
23      * {String} The URL of the image being requested. No default. Filled in by
24      * layer.getURL() function. 
25      */
26     url: null,
27     
28     /** 
29      * Property: imgDiv
30      * {DOMElement} The div element which wraps the image.
31      */
32     imgDiv: null,
33
34     /**
35      * Property: frame
36      * {DOMElement} The image element is appended to the frame.  Any gutter on
37      * the image will be hidden behind the frame. 
38      */ 
39     frame: null, 
40     
41     /**
42      * Property: layerAlphaHack
43      * {Boolean} True if the png alpha hack needs to be applied on the layer's div.
44      */
45     layerAlphaHack: null,
46     
47     /**
48      * Property: isBackBuffer
49      * {Boolean} Is this tile a back buffer tile?
50      */
51     isBackBuffer: false,
52     
53     /**
54      * Property: lastRatio
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.
58      */
59     lastRatio: 1,
60
61     /**
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.
69      */
70     isFirstDraw: true,
71         
72     /**
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.
76      */
77     backBufferTile: null,
78
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.
82      * 
83      * Constructor: OpenLayers.Tile.Image
84      * Constructor for a new <OpenLayers.Tile.Image> instance.
85      * 
86      * Parameters:
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>}
92      */   
93     initialize: function(layer, position, bounds, url, size) {
94         OpenLayers.Tile.prototype.initialize.apply(this, arguments);
95
96         this.url = url; //deprecated remove me
97         
98         this.frame = document.createElement('div'); 
99         this.frame.style.overflow = 'hidden'; 
100         this.frame.style.position = 'absolute'; 
101
102         this.layerAlphaHack = this.layer.alpha && OpenLayers.Util.alphaHack();
103     },
104
105     /** 
106      * APIMethod: destroy
107      * nullify references to prevent circular references and memory leaks
108      */
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);                
114             }
115
116             // unregister the "load" and "error" handlers. Only the "error" handler if
117             // this.layerAlphaHack is true.
118             OpenLayers.Event.stopObservingElement(this.imgDiv.id);
119             
120             if (this.imgDiv.parentNode == this.frame) {
121                 this.frame.removeChild(this.imgDiv);
122                 this.imgDiv.map = null;
123             }
124             this.imgDiv.urls = null;
125             // abort any currently loading image
126             this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
127         }
128         this.imgDiv = null;
129         if ((this.frame != null) && (this.frame.parentNode == this.layer.div)) { 
130             this.layer.div.removeChild(this.frame); 
131         }
132         this.frame = null; 
133         
134         /* clean up the backBufferTile if it exists */
135         if (this.backBufferTile) {
136             this.backBufferTile.destroy();
137             this.backBufferTile = null;
138         }
139         
140         this.layer.events.unregister("loadend", this, this.resetBackBuffer);
141         
142         OpenLayers.Tile.prototype.destroy.apply(this, arguments);
143     },
144
145     /**
146      * Method: clone
147      *
148      * Parameters:
149      * obj - {<OpenLayers.Tile.Image>} The tile to be cloned
150      *
151      * Returns:
152      * {<OpenLayers.Tile.Image>} An exact clone of this <OpenLayers.Tile.Image>
153      */
154     clone: function (obj) {
155         if (obj == null) {
156             obj = new OpenLayers.Tile.Image(this.layer, 
157                                             this.position, 
158                                             this.bounds, 
159                                             this.url, 
160                                             this.size);        
161         } 
162         
163         //pick up properties from superclass
164         obj = OpenLayers.Tile.prototype.clone.apply(this, [obj]);
165         
166         //dont want to directly copy the image div
167         obj.imgDiv = null;
168             
169         
170         return obj;
171     },
172     
173     /**
174      * Method: draw
175      * Check that a tile should be drawn, and draw it.
176      * 
177      * Returns:
178      * {Boolean} Always returns true.
179      */
180     draw: function() {
181         if (this.layer != this.layer.map.baseLayer && this.layer.reproject) {
182             this.bounds = this.getBoundsFromBaseLayer(this.position);
183         }
184         var drawTile = OpenLayers.Tile.prototype.draw.apply(this, arguments);
185         
186         if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, this.layer.transitionEffect) != -1) {
187             if (drawTile) {
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;
199                     
200                     // potentially end any transition effects when the tile loads
201                     this.events.register('loadend', this, this.resetBackBuffer);
202                     
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);
206                 }
207                 // run any transition effects
208                 this.startTransition();
209             } else {
210                 // if we aren't going to draw the tile, then the backBuffer should
211                 // be hidden too!
212                 if (this.backBufferTile) {
213                     this.backBufferTile.clear();
214                 }
215             }
216         } else {
217             if (drawTile && this.isFirstDraw) {
218                 this.events.register('loadend', this, this.showTile);
219                 this.isFirstDraw = false;
220             }   
221         }    
222         
223         if (!drawTile) {
224             return false;
225         }
226         
227         if (this.isLoading) {
228             //if we're already loading, send 'reload' instead of 'loadstart'.
229             this.events.triggerEvent("reload"); 
230         } else {
231             this.isLoading = true;
232             this.events.triggerEvent("loadstart");
233         }
234         
235         return this.renderTile();
236     },
237     
238     /** 
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 
243      *     foreground tile.
244      *
245      * Basic logic:
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 
250      *    foreground tile
251      */
252     resetBackBuffer: function() {
253         this.showTile();
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();
270             }
271
272             this.backBufferTile.hide();
273         }
274     },
275     
276     /**
277      * Method: renderTile
278      * Internal function to actually initialize the image tile,
279      *     position it correctly, and set its url.
280      */
281     renderTile: function() {
282         if (this.imgDiv == null) {
283             this.initImgDiv();
284         }
285
286         this.imgDiv.viewRequestID = this.layer.map.viewRequestID;
287         
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);
294         } else {
295             // syncronous image requests get the url and position the frame immediately,
296             // and don't wait for an image request to come back.
297           
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();
301             }
302           
303             this.url = this.layer.getURL(this.bounds);
304           
305             // position the frame immediately
306             this.positionImage(); 
307         }
308         return true;
309     },
310
311     /**
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
315      * code.
316      */
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 )
321             return;
322         
323         // position the frame 
324         OpenLayers.Util.modifyDOMElement(this.frame, 
325                                           null, this.position, this.size);   
326
327         var imageSize = this.layer.getImageSize(); 
328         if (this.layerAlphaHack) {
329             OpenLayers.Util.modifyAlphaImageDiv(this.imgDiv,
330                     null, null, imageSize, this.url);
331         } else {
332             OpenLayers.Util.modifyDOMElement(this.imgDiv,
333                     null, null, imageSize) ;
334             this.imgDiv.src = this.url;
335         }
336     },
337
338     /** 
339      * Method: clear
340      *  Clear the tile of any bounds/position-related data so that it can 
341      *   be reused in a new location.
342      */
343     clear: function() {
344         if(this.imgDiv) {
345             this.hide();
346             if (OpenLayers.Tile.Image.useBlankTile) { 
347                 this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
348             }    
349         }
350     },
351
352     /**
353      * Method: initImgDiv
354      * Creates the imgDiv property on the tile.
355      */
356     initImgDiv: function() {
357         
358         var offset = this.layer.imageOffset; 
359         var size = this.layer.getImageSize(); 
360      
361         if (this.layerAlphaHack) {
362             this.imgDiv = OpenLayers.Util.createAlphaImageDiv(null,
363                                                            offset,
364                                                            size,
365                                                            null,
366                                                            "relative",
367                                                            null,
368                                                            null,
369                                                            null,
370                                                            true);
371         } else {
372             this.imgDiv = OpenLayers.Util.createImage(null,
373                                                       offset,
374                                                       size,
375                                                       null,
376                                                       "relative",
377                                                       null,
378                                                       null,
379                                                       true);
380         }
381         
382         this.imgDiv.className = 'olTileImage';
383
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
388
389         OpenLayers.Event.observe( this.imgDiv, "load",
390             OpenLayers.Function.bind(this.checkImgURL, this) );
391         */
392         this.frame.style.zIndex = this.isBackBuffer ? 0 : 1;
393         this.frame.appendChild(this.imgDiv); 
394         this.layer.div.appendChild(this.frame); 
395
396         if(this.layer.opacity != null) {
397             
398             OpenLayers.Util.modifyDOMElement(this.imgDiv, null, null, null,
399                                              null, null, null, 
400                                              this.layer.opacity);
401         }
402
403         // we need this reference to check back the viewRequestID
404         this.imgDiv.map = this.layer.map;
405
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() {
409             
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
414             //
415             if (this.isLoading) { 
416                 this.isLoading = false; 
417                 this.events.triggerEvent("loadend"); 
418             }
419         };
420         
421         if (this.layerAlphaHack) { 
422             OpenLayers.Event.observe(this.imgDiv.childNodes[0], 'load', 
423                                      OpenLayers.Function.bind(onload, this));    
424         } else { 
425             OpenLayers.Event.observe(this.imgDiv, 'load', 
426                                  OpenLayers.Function.bind(onload, this)); 
427         } 
428         
429
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() {
433
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
438             // successfully.
439             if (this.imgDiv._attempts > OpenLayers.IMAGE_RELOAD_ATTEMPTS) {
440                 onload.call(this);
441             }
442         };
443         OpenLayers.Event.observe(this.imgDiv, "error",
444                                  OpenLayers.Function.bind(onerror, this));
445     },
446
447     /**
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.
455      * 
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.
460      * 
461      * See discussion in the thread at 
462      * http://openlayers.org/pipermail/dev/2007-January/000205.html
463      */
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.  
467         if (this.layer) {
468             var loaded = this.layerAlphaHack ? this.imgDiv.firstChild.src : this.imgDiv.src;
469             if (!OpenLayers.Util.isEquivalentUrl(loaded, this.url)) {
470                 this.hide();
471             }
472         }
473     },
474     
475     /**
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.
481      */
482     startTransition: function() {
483         // backBufferTile has to be valid and ready to use
484         if (!this.backBufferTile || !this.backBufferTile.imgDiv) {
485             return;
486         }
487
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
491         // each request.
492         var ratio = 1;
493         if (this.backBufferTile.resolution) {
494             ratio = this.backBufferTile.resolution / this.layer.getResolution();
495         }
496         
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 
502                 // backBufferTile.
503                 var upperLeft = new OpenLayers.LonLat(
504                     this.backBufferTile.bounds.left, 
505                     this.backBufferTile.bounds.top
506                 );
507                 var size = new OpenLayers.Size(
508                     this.backBufferTile.size.w * ratio,
509                     this.backBufferTile.size.h * ratio
510                 );
511
512                 var px = this.layer.map.getLayerPxFromLonLat(upperLeft);
513                 OpenLayers.Util.modifyDOMElement(this.backBufferTile.frame, 
514                                                  null, px, size);
515                 var imageSize = this.backBufferTile.imageSize;
516                 imageSize = new OpenLayers.Size(imageSize.w * ratio, 
517                                                 imageSize.h * ratio);
518                 var imageOffset = this.backBufferTile.imageOffset;
519                 if(imageOffset) {
520                     imageOffset = new OpenLayers.Pixel(
521                         imageOffset.x * ratio, imageOffset.y * ratio
522                     );
523                 }
524
525                 OpenLayers.Util.modifyDOMElement(
526                     this.backBufferTile.imgDiv, null, imageOffset, imageSize
527                 ) ;
528
529                 this.backBufferTile.show();
530             }
531         } else {
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();
538             } else {
539                 this.backBufferTile.hide();
540             }
541         }
542         this.lastRatio = ratio;
543
544     },
545     
546     /** 
547      * Method: show
548      * Show the tile by showing its frame.
549      */
550     show: function() {
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; 
558             } 
559         }
560     },
561     
562     /** 
563      * Method: hide
564      * Hide the tile by hiding its frame.
565      */
566     hide: function() {
567         this.frame.style.display = 'none';
568     },
569     
570     CLASS_NAME: "OpenLayers.Tile.Image"
571   }
572 );
573
574 OpenLayers.Tile.Image.useBlankTile = ( 
575     OpenLayers.Util.getBrowserName() == "safari" || 
576     OpenLayers.Util.getBrowserName() == "opera");