]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Layer.js
fixes notices
[syp.git] / openlayers / lib / OpenLayers / Layer.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/Map.js
8  * @requires OpenLayers/Projection.js
9  */
10
11 /**
12  * Class: OpenLayers.Layer
13  */
14 OpenLayers.Layer = OpenLayers.Class({
15
16     /**
17      * APIProperty: id
18      * {String}
19      */
20     id: null,
21
22     /** 
23      * APIProperty: name
24      * {String}
25      */
26     name: null,
27
28     /** 
29      * APIProperty: div
30      * {DOMElement}
31      */
32     div: null,
33
34     /**
35      * Property: opacity
36      * {Float} The layer's opacity. Float number between 0.0 and 1.0.
37      */
38     opacity: null,
39
40     /**
41      * APIProperty: alwaysInRange
42      * {Boolean} If a layer's display should not be scale-based, this should 
43      *     be set to true. This will cause the layer, as an overlay, to always 
44      *     be 'active', by always returning true from the calculateInRange() 
45      *     function. 
46      * 
47      *     If not explicitly specified for a layer, its value will be 
48      *     determined on startup in initResolutions() based on whether or not 
49      *     any scale-specific properties have been set as options on the 
50      *     layer. If no scale-specific options have been set on the layer, we 
51      *     assume that it should always be in range.
52      * 
53      *     See #987 for more info.
54      */
55     alwaysInRange: null,   
56
57     /**
58      * Constant: EVENT_TYPES
59      * {Array(String)} Supported application event types.  Register a listener
60      *     for a particular event with the following syntax:
61      * (code)
62      * layer.events.register(type, obj, listener);
63      * (end)
64      *
65      * Listeners will be called with a reference to an event object.  The
66      *     properties of this event depends on exactly what happened.
67      *
68      * All event objects have at least the following properties:
69      * object - {Object} A reference to layer.events.object.
70      * element - {DOMElement} A reference to layer.events.element.
71      *
72      * Supported map event types:
73      * loadstart - Triggered when layer loading starts.
74      * loadend - Triggered when layer loading ends.
75      * loadcancel - Triggered when layer loading is canceled.
76      * visibilitychanged - Triggered when layer visibility is changed.
77      * move - Triggered when layer moves (triggered with every mousemove
78      *     during a drag).
79      * moveend - Triggered when layer is done moving, object passed as
80      *     argument has a zoomChanged boolean property which tells that the
81      *     zoom has changed.
82      */
83     EVENT_TYPES: ["loadstart", "loadend", "loadcancel", "visibilitychanged",
84                   "move", "moveend"],
85         
86     /**
87      * APIProperty: events
88      * {<OpenLayers.Events>}
89      */
90     events: null,
91
92     /**
93      * APIProperty: map
94      * {<OpenLayers.Map>} This variable is set when the layer is added to 
95      *     the map, via the accessor function setMap().
96      */
97     map: null,
98     
99     /**
100      * APIProperty: isBaseLayer
101      * {Boolean} Whether or not the layer is a base layer. This should be set 
102      *     individually by all subclasses. Default is false
103      */
104     isBaseLayer: false,
105  
106     /**
107      * Property: alpha
108      * {Boolean} The layer's images have an alpha channel.  Default is false. 
109      */
110     alpha: false,
111
112     /** 
113      * APIProperty: displayInLayerSwitcher
114      * {Boolean} Display the layer's name in the layer switcher.  Default is
115      *     true.
116      */
117     displayInLayerSwitcher: true,
118
119     /**
120      * APIProperty: visibility
121      * {Boolean} The layer should be displayed in the map.  Default is true.
122      */
123     visibility: true,
124
125     /**
126      * APIProperty: attribution
127      * {String} Attribution string, displayed when an 
128      *     <OpenLayers.Control.Attribution> has been added to the map.
129      */
130     attribution: null, 
131
132     /** 
133      * Property: inRange
134      * {Boolean} The current map resolution is within the layer's min/max 
135      *     range. This is set in <OpenLayers.Map.setCenter> whenever the zoom 
136      *     changes.
137      */
138     inRange: false,
139     
140     /**
141      * Propery: imageSize
142      * {<OpenLayers.Size>} For layers with a gutter, the image is larger than 
143      *     the tile by twice the gutter in each dimension.
144      */
145     imageSize: null,
146     
147     /**
148      * Property: imageOffset
149      * {<OpenLayers.Pixel>} For layers with a gutter, the image offset 
150      *     represents displacement due to the gutter.
151      */
152     imageOffset: null,
153
154   // OPTIONS
155
156     /** 
157      * Property: options
158      * {Object} An optional object whose properties will be set on the layer.
159      *     Any of the layer properties can be set as a property of the options
160      *     object and sent to the constructor when the layer is created.
161      */
162     options: null,
163
164     /**
165      * APIProperty: eventListeners
166      * {Object} If set as an option at construction, the eventListeners
167      *     object will be registered with <OpenLayers.Events.on>.  Object
168      *     structure must be a listeners object as shown in the example for
169      *     the events.on method.
170      */
171     eventListeners: null,
172
173     /**
174      * APIProperty: gutter
175      * {Integer} Determines the width (in pixels) of the gutter around image
176      *     tiles to ignore.  By setting this property to a non-zero value,
177      *     images will be requested that are wider and taller than the tile
178      *     size by a value of 2 x gutter.  This allows artifacts of rendering
179      *     at tile edges to be ignored.  Set a gutter value that is equal to
180      *     half the size of the widest symbol that needs to be displayed.
181      *     Defaults to zero.  Non-tiled layers always have zero gutter.
182      */ 
183     gutter: 0, 
184
185     /**
186      * APIProperty: projection
187      * {<OpenLayers.Projection>} or {<String>} Set in the layer options to
188      *     override the default projection string this layer - also set maxExtent,
189      *     maxResolution, and units if appropriate. Can be either a string or
190      *     an <OpenLayers.Projection> object when created -- will be converted
191      *     to an object when setMap is called if a string is passed.  
192      */
193     projection: null,    
194     
195     /**
196      * APIProperty: units
197      * {String} The layer map units.  Defaults to 'degrees'.  Possible values
198      *     are 'degrees' (or 'dd'), 'm', 'ft', 'km', 'mi', 'inches'.
199      */
200     units: null,
201
202     /**
203      * APIProperty: scales
204      * {Array}  An array of map scales in descending order.  The values in the
205      *     array correspond to the map scale denominator.  Note that these
206      *     values only make sense if the display (monitor) resolution of the
207      *     client is correctly guessed by whomever is configuring the
208      *     application.  In addition, the units property must also be set.
209      *     Use <resolutions> instead wherever possible.
210      */
211     scales: null,
212
213     /**
214      * APIProperty: resolutions
215      * {Array} A list of map resolutions (map units per pixel) in descending
216      *     order.  If this is not set in the layer constructor, it will be set
217      *     based on other resolution related properties (maxExtent,
218      *     maxResolution, maxScale, etc.).
219      */
220     resolutions: null,
221     
222     /**
223      * APIProperty: maxExtent
224      * {<OpenLayers.Bounds>}  The center of these bounds will not stray outside
225      *     of the viewport extent during panning.  In addition, if
226      *     <displayOutsideMaxExtent> is set to false, data will not be
227      *     requested that falls completely outside of these bounds.
228      */
229     maxExtent: null,
230     
231     /**
232      * APIProperty: minExtent
233      * {<OpenLayers.Bounds>}
234      */
235     minExtent: null,
236     
237     /**
238      * APIProperty: maxResolution
239      * {Float} Default max is 360 deg / 256 px, which corresponds to
240      *     zoom level 0 on gmaps.  Specify a different value in the layer 
241      *     options if you are not using a geographic projection and 
242      *     displaying the whole world.
243      */
244     maxResolution: null,
245
246     /**
247      * APIProperty: minResolution
248      * {Float}
249      */
250     minResolution: null,
251
252     /**
253      * APIProperty: numZoomLevels
254      * {Integer}
255      */
256     numZoomLevels: null,
257    
258     /**
259      * APIProperty: minScale
260      * {Float}
261      */
262     minScale: null,
263     
264     /**
265      * APIProperty: maxScale
266      * {Float}
267      */
268     maxScale: null,
269
270     /**
271      * APIProperty: displayOutsideMaxExtent
272      * {Boolean} Request map tiles that are completely outside of the max 
273      *     extent for this layer. Defaults to false.
274      */
275     displayOutsideMaxExtent: false,
276
277     /**
278      * APIProperty: wrapDateLine
279      * {Boolean} #487 for more info.   
280      */
281     wrapDateLine: false,
282     
283     /**
284      * APIProperty: transitionEffect
285      * {String} The transition effect to use when the map is panned or
286      *     zoomed.  
287      *
288      * There are currently two supported values:
289      *  - *null* No transition effect (the default).
290      *  - *resize*  Existing tiles are resized on zoom to provide a visual
291      *    effect of the zoom having taken place immediately.  As the
292      *    new tiles become available, they are drawn over top of the
293      *    resized tiles.
294      */
295     transitionEffect: null,
296     
297     /**
298      * Property: SUPPORTED_TRANSITIONS
299      * {Array} An immutable (that means don't change it!) list of supported 
300      *     transitionEffect values.
301      */
302     SUPPORTED_TRANSITIONS: ['resize'],
303     
304     /**
305      * Constructor: OpenLayers.Layer
306      *
307      * Parameters:
308      * name - {String} The layer name
309      * options - {Object} Hashtable of extra options to tag onto the layer
310      */
311     initialize: function(name, options) {
312
313         this.addOptions(options);
314
315         this.name = name;
316         
317         if (this.id == null) {
318
319             this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_");
320
321             this.div = OpenLayers.Util.createDiv(this.id);
322             this.div.style.width = "100%";
323             this.div.style.height = "100%";
324             this.div.dir = "ltr";
325
326             this.events = new OpenLayers.Events(this, this.div, 
327                                                 this.EVENT_TYPES);
328             if(this.eventListeners instanceof Object) {
329                 this.events.on(this.eventListeners);
330             }
331
332         }
333
334         if (this.wrapDateLine) {
335             this.displayOutsideMaxExtent = true;
336         }
337     },
338     
339     /**
340      * Method: destroy
341      * Destroy is a destructor: this is to alleviate cyclic references which
342      *     the Javascript garbage cleaner can not take care of on its own.
343      *
344      * Parameters:
345      * setNewBaseLayer - {Boolean} Set a new base layer when this layer has
346      *     been destroyed.  Default is true.
347      */
348     destroy: function(setNewBaseLayer) {
349         if (setNewBaseLayer == null) {
350             setNewBaseLayer = true;
351         }
352         if (this.map != null) {
353             this.map.removeLayer(this, setNewBaseLayer);
354         }
355         this.projection = null;
356         this.map = null;
357         this.name = null;
358         this.div = null;
359         this.options = null;
360
361         if (this.events) {
362             if(this.eventListeners) {
363                 this.events.un(this.eventListeners);
364             }
365             this.events.destroy();
366         }
367         this.eventListeners = null;
368         this.events = null;
369     },
370     
371    /**
372     * Method: clone
373     *
374     * Parameters:
375     * obj - {<OpenLayers.Layer>} The layer to be cloned
376     *
377     * Returns:
378     * {<OpenLayers.Layer>} An exact clone of this <OpenLayers.Layer>
379     */
380     clone: function (obj) {
381         
382         if (obj == null) {
383             obj = new OpenLayers.Layer(this.name, this.options);
384         } 
385         
386         // catch any randomly tagged-on properties
387         OpenLayers.Util.applyDefaults(obj, this);
388         
389         // a cloned layer should never have its map property set
390         //  because it has not been added to a map yet. 
391         obj.map = null;
392         
393         return obj;
394     },
395     
396     /** 
397      * APIMethod: setName
398      * Sets the new layer name for this layer.  Can trigger a changelayer event
399      *     on the map.
400      *
401      * Parameters:
402      * newName - {String} The new name.
403      */
404     setName: function(newName) {
405         if (newName != this.name) {
406             this.name = newName;
407             if (this.map != null) {
408                 this.map.events.triggerEvent("changelayer", {
409                     layer: this,
410                     property: "name"
411                 });
412             }
413         }
414     },    
415     
416    /**
417     * APIMethod: addOptions
418     * 
419     * Parameters:
420     * newOptions - {Object}
421     */
422     addOptions: function (newOptions) {
423         
424         if (this.options == null) {
425             this.options = {};
426         }
427         
428         // update our copy for clone
429         OpenLayers.Util.extend(this.options, newOptions);
430
431         // add new options to this
432         OpenLayers.Util.extend(this, newOptions);
433     },
434     
435     /**
436      * APIMethod: onMapResize
437      * This function can be implemented by subclasses
438      */
439     onMapResize: function() {
440         //this function can be implemented by subclasses  
441     },
442
443     /**
444      * APIMethod: redraw
445      * Redraws the layer.  Returns true if the layer was redrawn, false if not.
446      *
447      * Returns:
448      * {Boolean} The layer was redrawn.
449      */
450     redraw: function() {
451         var redrawn = false;
452         if (this.map) {
453
454             // min/max Range may have changed
455             this.inRange = this.calculateInRange();
456
457             // map's center might not yet be set
458             var extent = this.getExtent();
459
460             if (extent && this.inRange && this.visibility) {
461                 var zoomChanged = true;
462                 this.moveTo(extent, zoomChanged, false);
463                 this.events.triggerEvent("moveend",
464                     {"zoomChanged": zoomChanged});
465                 redrawn = true;
466             }
467         }
468         return redrawn;
469     },
470
471     /**
472      * Method: moveTo
473      * 
474      * Parameters:
475      * bound - {<OpenLayers.Bounds>}
476      * zoomChanged - {Boolean} Tells when zoom has changed, as layers have to
477      *     do some init work in that case.
478      * dragging - {Boolean}
479      */
480     moveTo:function(bounds, zoomChanged, dragging) {
481         var display = this.visibility;
482         if (!this.isBaseLayer) {
483             display = display && this.inRange;
484         }
485         this.display(display);
486     },
487
488     /**
489      * Method: setMap
490      * Set the map property for the layer. This is done through an accessor
491      *     so that subclasses can override this and take special action once 
492      *     they have their map variable set. 
493      * 
494      *     Here we take care to bring over any of the necessary default 
495      *     properties from the map. 
496      * 
497      * Parameters:
498      * map - {<OpenLayers.Map>}
499      */
500     setMap: function(map) {
501         if (this.map == null) {
502         
503             this.map = map;
504             
505             // grab some essential layer data from the map if it hasn't already
506             //  been set
507             this.maxExtent = this.maxExtent || this.map.maxExtent;
508             this.projection = this.projection || this.map.projection;
509             
510             if (this.projection && typeof this.projection == "string") {
511                 this.projection = new OpenLayers.Projection(this.projection);
512             }
513             
514             // Check the projection to see if we can get units -- if not, refer
515             // to properties.
516             this.units = this.projection.getUnits() ||
517                          this.units || this.map.units;
518             
519             this.initResolutions();
520             
521             if (!this.isBaseLayer) {
522                 this.inRange = this.calculateInRange();
523                 var show = ((this.visibility) && (this.inRange));
524                 this.div.style.display = show ? "" : "none";
525             }
526             
527             // deal with gutters
528             this.setTileSize();
529         }
530     },
531     
532     /**
533      * Method: afterAdd
534      * Called at the end of the map.addLayer sequence.  At this point, the map
535      *     will have a base layer.  To be overridden by subclasses.
536      */
537     afterAdd: function() {
538     },
539     
540     /**
541      * APIMethod: removeMap
542      * Just as setMap() allows each layer the possibility to take a 
543      *     personalized action on being added to the map, removeMap() allows
544      *     each layer to take a personalized action on being removed from it. 
545      *     For now, this will be mostly unused, except for the EventPane layer,
546      *     which needs this hook so that it can remove the special invisible
547      *     pane. 
548      * 
549      * Parameters:
550      * map - {<OpenLayers.Map>}
551      */
552     removeMap: function(map) {
553         //to be overridden by subclasses
554     },
555     
556     /**
557      * APIMethod: getImageSize
558      * 
559      * Returns:
560      * {<OpenLayers.Size>} The size that the image should be, taking into 
561      *     account gutters.
562      */ 
563     getImageSize: function() { 
564         return (this.imageSize || this.tileSize); 
565     },    
566   
567     /**
568      * APIMethod: setTileSize
569      * Set the tile size based on the map size.  This also sets layer.imageSize
570      *     and layer.imageOffset for use by Tile.Image.
571      * 
572      * Parameters:
573      * size - {<OpenLayers.Size>}
574      */
575     setTileSize: function(size) {
576         var tileSize = (size) ? size :
577                                 ((this.tileSize) ? this.tileSize :
578                                                    this.map.getTileSize());
579         this.tileSize = tileSize;
580         if(this.gutter) {
581           // layers with gutters need non-null tile sizes
582           //if(tileSize == null) {
583           //    OpenLayers.console.error("Error in layer.setMap() for " +
584           //                              this.name + ": layers with " +
585           //                              "gutters need non-null tile sizes");
586           //}
587             this.imageOffset = new OpenLayers.Pixel(-this.gutter, 
588                                                     -this.gutter); 
589             this.imageSize = new OpenLayers.Size(tileSize.w + (2*this.gutter), 
590                                                  tileSize.h + (2*this.gutter)); 
591         }
592     },
593
594     /**
595      * APIMethod: getVisibility
596      * 
597      * Returns:
598      * {Boolean} The layer should be displayed (if in range).
599      */
600     getVisibility: function() {
601         return this.visibility;
602     },
603
604     /** 
605      * APIMethod: setVisibility
606      * Set the visibility flag for the layer and hide/show & redraw 
607      *     accordingly. Fire event unless otherwise specified
608      * 
609      * Note that visibility is no longer simply whether or not the layer's
610      *     style.display is set to "block". Now we store a 'visibility' state 
611      *     property on the layer class, this allows us to remember whether or 
612      *     not we *desire* for a layer to be visible. In the case where the 
613      *     map's resolution is out of the layer's range, this desire may be 
614      *     subverted.
615      * 
616      * Parameters:
617      * visible - {Boolean} Whether or not to display the layer (if in range)
618      */
619     setVisibility: function(visibility) {
620         if (visibility != this.visibility) {
621             this.visibility = visibility;
622             this.display(visibility);
623             this.redraw();
624             if (this.map != null) {
625                 this.map.events.triggerEvent("changelayer", {
626                     layer: this,
627                     property: "visibility"
628                 });
629             }
630             this.events.triggerEvent("visibilitychanged");
631         }
632     },
633
634     /** 
635      * APIMethod: display
636      * Hide or show the Layer
637      * 
638      * Parameters:
639      * display - {Boolean}
640      */
641     display: function(display) {
642         var inRange = this.calculateInRange();
643         if (display != (this.div.style.display != "none")) {
644             this.div.style.display = (display && inRange) ? "block" : "none";
645         }
646     },
647
648     /**
649      * APIMethod: calculateInRange
650      * 
651      * Returns:
652      * {Boolean} The layer is displayable at the current map's current
653      *     resolution. Note that if 'alwaysInRange' is true for the layer, 
654      *     this function will always return true.
655      */
656     calculateInRange: function() {
657         var inRange = false;
658
659         if (this.alwaysInRange) {
660             inRange = true;
661         } else {
662             if (this.map) {
663                 var resolution = this.map.getResolution();
664                 inRange = ( (resolution >= this.minResolution) &&
665                             (resolution <= this.maxResolution) );
666             }
667         }
668         return inRange;
669     },
670
671     /** 
672      * APIMethod: setIsBaseLayer
673      * 
674      * Parameters:
675      * isBaseLayer - {Boolean}
676      */
677     setIsBaseLayer: function(isBaseLayer) {
678         if (isBaseLayer != this.isBaseLayer) {
679             this.isBaseLayer = isBaseLayer;
680             if (this.map != null) {
681                 this.map.events.triggerEvent("changebaselayer", {
682                     layer: this
683                 });
684             }
685         }
686     },
687
688   /********************************************************/
689   /*                                                      */
690   /*                 Baselayer Functions                  */
691   /*                                                      */
692   /********************************************************/
693   
694     /** 
695      * Method: initResolutions
696      * This method's responsibility is to set up the 'resolutions' array 
697      *     for the layer -- this array is what the layer will use to interface
698      *     between the zoom levels of the map and the resolution display 
699      *     of the layer.
700      * 
701      * The user has several options that determine how the array is set up.
702      *  
703      * For a detailed explanation, see the following wiki from the 
704      *     openlayers.org homepage:
705      *     http://trac.openlayers.org/wiki/SettingZoomLevels
706      */
707     initResolutions: function() {
708
709         // These are the relevant options which are used for calculating 
710         //  resolutions information.
711         //
712         var props = new Array(
713           'projection', 'units',
714           'scales', 'resolutions',
715           'maxScale', 'minScale', 
716           'maxResolution', 'minResolution', 
717           'minExtent', 'maxExtent',
718           'numZoomLevels', 'maxZoomLevel'
719         );
720
721         //these are the properties which do *not* imply that user wishes 
722         // this layer to be scale-dependant
723         var notScaleProps = ['projection', 'units'];    
724
725         //should the layer be scale-dependant? default is false -- this will 
726         // only be set true if we find that the user has specified a property
727         // from the 'props' array that is not in 'notScaleProps'
728         var useInRange = false;
729
730         // First we create a new object where we will store all of the 
731         //  resolution-related properties that we find in either the layer's
732         //  'options' array or from the map.
733         //
734         var confProps = {};        
735         for(var i=0, len=props.length; i<len; i++) {
736             var property = props[i];
737             
738             // If the layer had one of these properties set *and* it is 
739             // a scale property (is not a non-scale property), then we assume
740             // the user did intend to use scale-dependant display (useInRange).
741             if (this.options[property] && 
742                 OpenLayers.Util.indexOf(notScaleProps, property) == -1) {
743                 useInRange = true;
744             }
745                    
746             confProps[property] = this.options[property] || this.map[property];
747         }
748
749         //only automatically set 'alwaysInRange' if the user hasn't already 
750         // set it (to true or false, since the default is null). If user did
751         // not intend to use scale-dependant display then we set they layer
752         // as alwaysInRange. This means calculateInRange() will always return 
753         // true and the layer will never be turned off due to scale changes.
754         //
755         if (this.alwaysInRange == null) {
756             this.alwaysInRange = !useInRange;
757         }
758
759         // Do not use the scales array set at the map level if 
760         // either minScale or maxScale or both are set at the
761         // layer level
762         if ((this.options.minScale != null ||
763              this.options.maxScale != null) &&
764             this.options.scales == null) {
765
766             confProps.scales = null;
767         }
768         // Do not use the resolutions array set at the map level if 
769         // either minResolution or maxResolution or both are set at the
770         // layer level
771         if ((this.options.minResolution != null ||
772              this.options.maxResolution != null) &&
773             this.options.resolutions == null) {
774
775             confProps.resolutions = null;
776         }
777
778         // If numZoomLevels hasn't been set and the maxZoomLevel *has*, 
779         //  then use maxZoomLevel to calculate numZoomLevels
780         //
781         if ( (!confProps.numZoomLevels) && (confProps.maxZoomLevel) ) {
782             confProps.numZoomLevels = confProps.maxZoomLevel + 1;
783         }
784
785         // First off, we take whatever hodge-podge of values we have and 
786         //  calculate/distill them down into a resolutions[] array
787         //
788         if ((confProps.scales != null) || (confProps.resolutions != null)) {
789           //preset levels
790             if (confProps.scales != null) {
791                 confProps.resolutions = [];
792                 for(var i=0, len=confProps.scales.length; i<len; i++) {
793                     var scale = confProps.scales[i];
794                     confProps.resolutions[i] = 
795                        OpenLayers.Util.getResolutionFromScale(scale, 
796                                                               confProps.units);
797                 }
798             }
799             confProps.numZoomLevels = confProps.resolutions.length;
800
801         } else {
802           //maxResolution and numZoomLevels based calculation
803
804             // determine maxResolution
805             if (confProps.minScale) {
806                 confProps.maxResolution = 
807                     OpenLayers.Util.getResolutionFromScale(confProps.minScale, 
808                                                            confProps.units);
809             } else if (confProps.maxResolution == "auto") {
810                 var viewSize = this.map.getSize();
811                 var wRes = confProps.maxExtent.getWidth() / viewSize.w;
812                 var hRes = confProps.maxExtent.getHeight()/ viewSize.h;
813                 confProps.maxResolution = Math.max(wRes, hRes);
814             } 
815
816             // determine minResolution
817             if (confProps.maxScale != null) {           
818                 confProps.minResolution = 
819                     OpenLayers.Util.getResolutionFromScale(confProps.maxScale, 
820                                                            confProps.units);
821             } else if ( (confProps.minResolution == "auto") && 
822                         (confProps.minExtent != null) ) {
823                 var viewSize = this.map.getSize();
824                 var wRes = confProps.minExtent.getWidth() / viewSize.w;
825                 var hRes = confProps.minExtent.getHeight()/ viewSize.h;
826                 confProps.minResolution = Math.max(wRes, hRes);
827             } 
828
829             // determine numZoomLevels if not already set on the layer
830             // this gives numZoomLevels assuming approximately base 2 scaling
831             if (confProps.minResolution != null &&
832                 this.options.numZoomLevels == undefined) {
833                 var ratio = confProps.maxResolution / confProps.minResolution;
834                 confProps.numZoomLevels = 
835                     Math.floor(Math.log(ratio) / Math.log(2)) + 1;
836             }
837             
838             // now we have numZoomLevels and maxResolution, 
839             //  we can populate the resolutions array
840             confProps.resolutions = new Array(confProps.numZoomLevels);
841             var base = 2;
842             if(typeof confProps.minResolution == "number" &&
843                confProps.numZoomLevels > 1) {
844                 /**
845                  * If maxResolution and minResolution are set (or related
846                  * scale properties), we calculate the base for exponential
847                  * scaling that starts at maxResolution and ends at
848                  * minResolution in numZoomLevels steps.
849                  */
850                 base = Math.pow(
851                     (confProps.maxResolution / confProps.minResolution),
852                     (1 / (confProps.numZoomLevels - 1))
853                 );
854             }
855             for (var i=0; i < confProps.numZoomLevels; i++) {
856                 var res = confProps.maxResolution / Math.pow(base, i);
857                 confProps.resolutions[i] = res;
858             }
859         }
860         
861         //sort resolutions array ascendingly
862         //
863         confProps.resolutions.sort( function(a, b) { return(b-a); } );
864
865         // now set our newly calculated values back to the layer 
866         //  Note: We specifically do *not* set them to layer.options, which we 
867         //        will preserve as it was when we added this layer to the map. 
868         //        this way cloned layers reset themselves to new map div 
869         //        dimensions)
870         //
871
872         this.resolutions = confProps.resolutions;
873         this.maxResolution = confProps.resolutions[0];
874         var lastIndex = confProps.resolutions.length - 1;
875         this.minResolution = confProps.resolutions[lastIndex];
876         
877         this.scales = [];
878         for(var i=0, len=confProps.resolutions.length; i<len; i++) {
879             this.scales[i] = 
880                OpenLayers.Util.getScaleFromResolution(confProps.resolutions[i], 
881                                                       confProps.units);
882         }
883         this.minScale = this.scales[0];
884         this.maxScale = this.scales[this.scales.length - 1];
885         
886         this.numZoomLevels = confProps.numZoomLevels;
887     },
888
889     /**
890      * APIMethod: getResolution
891      * 
892      * Returns:
893      * {Float} The currently selected resolution of the map, taken from the
894      *     resolutions array, indexed by current zoom level.
895      */
896     getResolution: function() {
897         var zoom = this.map.getZoom();
898         return this.getResolutionForZoom(zoom);
899     },
900
901     /** 
902      * APIMethod: getExtent
903      * 
904      * Returns:
905      * {<OpenLayers.Bounds>} A Bounds object which represents the lon/lat 
906      *     bounds of the current viewPort.
907      */
908     getExtent: function() {
909         // just use stock map calculateBounds function -- passing no arguments
910         //  means it will user map's current center & resolution
911         //
912         return this.map.calculateBounds();
913     },
914
915     /**
916      * APIMethod: getZoomForExtent
917      * 
918      * Parameters:
919      * bounds - {<OpenLayers.Bounds>}
920      * closest - {Boolean} Find the zoom level that most closely fits the 
921      *     specified bounds. Note that this may result in a zoom that does 
922      *     not exactly contain the entire extent.
923      *     Default is false.
924      *
925      * Returns:
926      * {Integer} The index of the zoomLevel (entry in the resolutions array) 
927      *     for the passed-in extent. We do this by calculating the ideal 
928      *     resolution for the given extent (based on the map size) and then 
929      *     calling getZoomForResolution(), passing along the 'closest'
930      *     parameter.
931      */
932     getZoomForExtent: function(extent, closest) {
933         var viewSize = this.map.getSize();
934         var idealResolution = Math.max( extent.getWidth()  / viewSize.w,
935                                         extent.getHeight() / viewSize.h );
936
937         return this.getZoomForResolution(idealResolution, closest);
938     },
939     
940     /** 
941      * Method: getDataExtent
942      * Calculates the max extent which includes all of the data for the layer.
943      *     This function is to be implemented by subclasses.
944      * 
945      * Returns:
946      * {<OpenLayers.Bounds>}
947      */
948     getDataExtent: function () {
949         //to be implemented by subclasses
950     },
951
952     /**
953      * APIMethod: getResolutionForZoom
954      * 
955      * Parameter:
956      * zoom - {Float}
957      * 
958      * Returns:
959      * {Float} A suitable resolution for the specified zoom.
960      */
961     getResolutionForZoom: function(zoom) {
962         zoom = Math.max(0, Math.min(zoom, this.resolutions.length - 1));
963         var resolution;
964         if(this.map.fractionalZoom) {
965             var low = Math.floor(zoom);
966             var high = Math.ceil(zoom);
967             resolution = this.resolutions[low] -
968                 ((zoom-low) * (this.resolutions[low]-this.resolutions[high]));
969         } else {
970             resolution = this.resolutions[Math.round(zoom)];
971         }
972         return resolution;
973     },
974
975     /**
976      * APIMethod: getZoomForResolution
977      * 
978      * Parameters:
979      * resolution - {Float}
980      * closest - {Boolean} Find the zoom level that corresponds to the absolute 
981      *     closest resolution, which may result in a zoom whose corresponding
982      *     resolution is actually smaller than we would have desired (if this
983      *     is being called from a getZoomForExtent() call, then this means that
984      *     the returned zoom index might not actually contain the entire 
985      *     extent specified... but it'll be close).
986      *     Default is false.
987      * 
988      * Returns:
989      * {Integer} The index of the zoomLevel (entry in the resolutions array) 
990      *     that corresponds to the best fit resolution given the passed in 
991      *     value and the 'closest' specification.
992      */
993     getZoomForResolution: function(resolution, closest) {
994         var zoom;
995         if(this.map.fractionalZoom) {
996             var lowZoom = 0;
997             var highZoom = this.resolutions.length - 1;
998             var highRes = this.resolutions[lowZoom];
999             var lowRes = this.resolutions[highZoom];
1000             var res;
1001             for(var i=0, len=this.resolutions.length; i<len; ++i) {
1002                 res = this.resolutions[i];
1003                 if(res >= resolution) {
1004                     highRes = res;
1005                     lowZoom = i;
1006                 }
1007                 if(res <= resolution) {
1008                     lowRes = res;
1009                     highZoom = i;
1010                     break;
1011                 }
1012             }
1013             var dRes = highRes - lowRes;
1014             if(dRes > 0) {
1015                 zoom = lowZoom + ((highRes - resolution) / dRes);
1016             } else {
1017                 zoom = lowZoom;
1018             }
1019         } else {
1020             var diff;
1021             var minDiff = Number.POSITIVE_INFINITY;
1022             for(var i=0, len=this.resolutions.length; i<len; i++) {            
1023                 if (closest) {
1024                     diff = Math.abs(this.resolutions[i] - resolution);
1025                     if (diff > minDiff) {
1026                         break;
1027                     }
1028                     minDiff = diff;
1029                 } else {
1030                     if (this.resolutions[i] < resolution) {
1031                         break;
1032                     }
1033                 }
1034             }
1035             zoom = Math.max(0, i-1);
1036         }
1037         return zoom;
1038     },
1039     
1040     /**
1041      * APIMethod: getLonLatFromViewPortPx
1042      * 
1043      * Parameters:
1044      * viewPortPx - {<OpenLayers.Pixel>}
1045      *
1046      * Returns:
1047      * {<OpenLayers.LonLat>} An OpenLayers.LonLat which is the passed-in 
1048      *     view port <OpenLayers.Pixel>, translated into lon/lat by the layer.
1049      */
1050     getLonLatFromViewPortPx: function (viewPortPx) {
1051         var lonlat = null;
1052         if (viewPortPx != null) {
1053             var size = this.map.getSize();
1054             var center = this.map.getCenter();
1055             if (center) {
1056                 var res  = this.map.getResolution();
1057         
1058                 var delta_x = viewPortPx.x - (size.w / 2);
1059                 var delta_y = viewPortPx.y - (size.h / 2);
1060             
1061                 lonlat = new OpenLayers.LonLat(center.lon + delta_x * res ,
1062                                              center.lat - delta_y * res); 
1063
1064                 if (this.wrapDateLine) {
1065                     lonlat = lonlat.wrapDateLine(this.maxExtent);
1066                 }
1067             } // else { DEBUG STATEMENT }
1068         }
1069         return lonlat;
1070     },
1071
1072     /**
1073      * APIMethod: getViewPortPxFromLonLat
1074      * Returns a pixel location given a map location.  This method will return
1075      *     fractional pixel values.
1076      * 
1077      * Parameters:
1078      * lonlat - {<OpenLayers.LonLat>}
1079      *
1080      * Returns: 
1081      * {<OpenLayers.Pixel>} An <OpenLayers.Pixel> which is the passed-in 
1082      *     <OpenLayers.LonLat>,translated into view port pixels.
1083      */
1084     getViewPortPxFromLonLat: function (lonlat) {
1085         var px = null; 
1086         if (lonlat != null) {
1087             var resolution = this.map.getResolution();
1088             var extent = this.map.getExtent();
1089             px = new OpenLayers.Pixel(
1090                 (1/resolution * (lonlat.lon - extent.left)),
1091                 (1/resolution * (extent.top - lonlat.lat))
1092             );    
1093         }
1094         return px;
1095     },
1096     
1097     /**
1098      * APIMethod: setOpacity
1099      * Sets the opacity for the entire layer (all images)
1100      * 
1101      * Parameter:
1102      * opacity - {Float}
1103      */
1104     setOpacity: function(opacity) {
1105         if (opacity != this.opacity) {
1106             this.opacity = opacity;
1107             for(var i=0, len=this.div.childNodes.length; i<len; ++i) {
1108                 var element = this.div.childNodes[i].firstChild;
1109                 OpenLayers.Util.modifyDOMElement(element, null, null, null, 
1110                                                  null, null, null, opacity);
1111             }
1112         }
1113     },
1114
1115     /**
1116      * Method: getZIndex
1117      * 
1118      * Returns: 
1119      * {Integer} the z-index of this layer
1120      */    
1121     getZIndex: function () {
1122         return this.div.style.zIndex;
1123     },
1124
1125     /**
1126      * Method: setZIndex
1127      * 
1128      * Parameters: 
1129      * zIndex - {Integer}
1130      */    
1131     setZIndex: function (zIndex) {
1132         this.div.style.zIndex = zIndex;
1133     },
1134
1135     /**
1136      * Method: adjustBounds
1137      * This function will take a bounds, and if wrapDateLine option is set
1138      *     on the layer, it will return a bounds which is wrapped around the 
1139      *     world. We do not wrap for bounds which *cross* the 
1140      *     maxExtent.left/right, only bounds which are entirely to the left 
1141      *     or entirely to the right.
1142      * 
1143      * Parameters:
1144      * bounds - {<OpenLayers.Bounds>}
1145      */
1146     adjustBounds: function (bounds) {
1147
1148         if (this.gutter) {
1149             // Adjust the extent of a bounds in map units by the 
1150             // layer's gutter in pixels.
1151             var mapGutter = this.gutter * this.map.getResolution();
1152             bounds = new OpenLayers.Bounds(bounds.left - mapGutter,
1153                                            bounds.bottom - mapGutter,
1154                                            bounds.right + mapGutter,
1155                                            bounds.top + mapGutter);
1156         }
1157
1158         if (this.wrapDateLine) {
1159             // wrap around the date line, within the limits of rounding error
1160             var wrappingOptions = { 
1161                 'rightTolerance':this.getResolution()
1162             };    
1163             bounds = bounds.wrapDateLine(this.maxExtent, wrappingOptions);
1164                               
1165         }
1166         return bounds;
1167     },
1168
1169     CLASS_NAME: "OpenLayers.Layer"
1170 });