diff options
Diffstat (limited to 'misc/openlayers/lib/OpenLayers/Layer.js')
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Layer.js | 1377 |
1 files changed, 1377 insertions, 0 deletions
diff --git a/misc/openlayers/lib/OpenLayers/Layer.js b/misc/openlayers/lib/OpenLayers/Layer.js new file mode 100644 index 0000000..3bd4186 --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Layer.js @@ -0,0 +1,1377 @@ +/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. */ + + +/** + * @requires OpenLayers/BaseTypes/Class.js + * @requires OpenLayers/Map.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Layer + */ +OpenLayers.Layer = OpenLayers.Class({ + + /** + * APIProperty: id + * {String} + */ + id: null, + + /** + * APIProperty: name + * {String} + */ + name: null, + + /** + * APIProperty: div + * {DOMElement} + */ + div: null, + + /** + * APIProperty: opacity + * {Float} The layer's opacity. Float number between 0.0 and 1.0. Default + * is 1. + */ + opacity: 1, + + /** + * APIProperty: alwaysInRange + * {Boolean} If a layer's display should not be scale-based, this should + * be set to true. This will cause the layer, as an overlay, to always + * be 'active', by always returning true from the calculateInRange() + * function. + * + * If not explicitly specified for a layer, its value will be + * determined on startup in initResolutions() based on whether or not + * any scale-specific properties have been set as options on the + * layer. If no scale-specific options have been set on the layer, we + * assume that it should always be in range. + * + * See #987 for more info. + */ + alwaysInRange: null, + + /** + * Constant: RESOLUTION_PROPERTIES + * {Array} The properties that are used for calculating resolutions + * information. + */ + RESOLUTION_PROPERTIES: [ + 'scales', 'resolutions', + 'maxScale', 'minScale', + 'maxResolution', 'minResolution', + 'numZoomLevels', 'maxZoomLevel' + ], + + /** + * APIProperty: events + * {<OpenLayers.Events>} + * + * Register a listener for a particular event with the following syntax: + * (code) + * layer.events.register(type, obj, listener); + * (end) + * + * Listeners will be called with a reference to an event object. The + * properties of this event depends on exactly what happened. + * + * All event objects have at least the following properties: + * object - {Object} A reference to layer.events.object. + * element - {DOMElement} A reference to layer.events.element. + * + * Supported map event types: + * loadstart - Triggered when layer loading starts. When using a Vector + * layer with a Fixed or BBOX strategy, the event object includes + * a *filter* property holding the OpenLayers.Filter used when + * calling read on the protocol. + * loadend - Triggered when layer loading ends. When using a Vector layer + * with a Fixed or BBOX strategy, the event object includes a + * *response* property holding an OpenLayers.Protocol.Response object. + * visibilitychanged - Triggered when the layer's visibility property is + * changed, e.g. by turning the layer on or off in the layer switcher. + * Note that the actual visibility of the layer can also change if it + * gets out of range (see <calculateInRange>). If you also want to catch + * these cases, register for the map's 'changelayer' event instead. + * move - Triggered when layer moves (triggered with every mousemove + * during a drag). + * moveend - Triggered when layer is done moving, object passed as + * argument has a zoomChanged boolean property which tells that the + * zoom has changed. + * added - Triggered after the layer is added to a map. Listeners will + * receive an object with a *map* property referencing the map and a + * *layer* property referencing the layer. + * removed - Triggered after the layer is removed from the map. Listeners + * will receive an object with a *map* property referencing the map and + * a *layer* property referencing the layer. + */ + events: null, + + /** + * APIProperty: map + * {<OpenLayers.Map>} This variable is set when the layer is added to + * the map, via the accessor function setMap(). + */ + map: null, + + /** + * APIProperty: isBaseLayer + * {Boolean} Whether or not the layer is a base layer. This should be set + * individually by all subclasses. Default is false + */ + isBaseLayer: false, + + /** + * Property: alpha + * {Boolean} The layer's images have an alpha channel. Default is false. + */ + alpha: false, + + /** + * APIProperty: displayInLayerSwitcher + * {Boolean} Display the layer's name in the layer switcher. Default is + * true. + */ + displayInLayerSwitcher: true, + + /** + * APIProperty: visibility + * {Boolean} The layer should be displayed in the map. Default is true. + */ + visibility: true, + + /** + * APIProperty: attribution + * {String} Attribution string, displayed when an + * <OpenLayers.Control.Attribution> has been added to the map. + */ + attribution: null, + + /** + * Property: inRange + * {Boolean} The current map resolution is within the layer's min/max + * range. This is set in <OpenLayers.Map.setCenter> whenever the zoom + * changes. + */ + inRange: false, + + /** + * Propery: imageSize + * {<OpenLayers.Size>} For layers with a gutter, the image is larger than + * the tile by twice the gutter in each dimension. + */ + imageSize: null, + + // OPTIONS + + /** + * Property: options + * {Object} An optional object whose properties will be set on the layer. + * Any of the layer properties can be set as a property of the options + * object and sent to the constructor when the layer is created. + */ + options: null, + + /** + * APIProperty: eventListeners + * {Object} If set as an option at construction, the eventListeners + * object will be registered with <OpenLayers.Events.on>. Object + * structure must be a listeners object as shown in the example for + * the events.on method. + */ + eventListeners: null, + + /** + * APIProperty: gutter + * {Integer} Determines the width (in pixels) of the gutter around image + * tiles to ignore. By setting this property to a non-zero value, + * images will be requested that are wider and taller than the tile + * size by a value of 2 x gutter. This allows artifacts of rendering + * at tile edges to be ignored. Set a gutter value that is equal to + * half the size of the widest symbol that needs to be displayed. + * Defaults to zero. Non-tiled layers always have zero gutter. + */ + gutter: 0, + + /** + * APIProperty: projection + * {<OpenLayers.Projection>} or {<String>} Specifies the projection of the layer. + * Can be set in the layer options. If not specified in the layer options, + * it is set to the default projection specified in the map, + * when the layer is added to the map. + * Projection along with default maxExtent and resolutions + * are set automatically with commercial baselayers in EPSG:3857, + * such as Google, Bing and OpenStreetMap, and do not need to be specified. + * Otherwise, if specifying projection, also set maxExtent, + * maxResolution or resolutions as appropriate. + * When using vector layers with strategies, layer projection should be set + * to the projection of the source data if that is different from the map default. + * + * Can be either a string or an <OpenLayers.Projection> object; + * if a string is passed, will be converted to an object when + * the layer is added to the map. + * + */ + projection: null, + + /** + * APIProperty: units + * {String} The layer map units. Defaults to null. Possible values + * are 'degrees' (or 'dd'), 'm', 'ft', 'km', 'mi', 'inches'. + * Normally taken from the projection. + * Only required if both map and layers do not define a projection, + * or if they define a projection which does not define units. + */ + units: null, + + /** + * APIProperty: scales + * {Array} An array of map scales in descending order. The values in the + * array correspond to the map scale denominator. Note that these + * values only make sense if the display (monitor) resolution of the + * client is correctly guessed by whomever is configuring the + * application. In addition, the units property must also be set. + * Use <resolutions> instead wherever possible. + */ + scales: null, + + /** + * APIProperty: resolutions + * {Array} A list of map resolutions (map units per pixel) in descending + * order. If this is not set in the layer constructor, it will be set + * based on other resolution related properties (maxExtent, + * maxResolution, maxScale, etc.). + */ + resolutions: null, + + /** + * APIProperty: maxExtent + * {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * The maximum extent for the layer. Defaults to null. + * + * The center of these bounds will not stray outside + * of the viewport extent during panning. In addition, if + * <displayOutsideMaxExtent> is set to false, data will not be + * requested that falls completely outside of these bounds. + */ + maxExtent: null, + + /** + * APIProperty: minExtent + * {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * The minimum extent for the layer. Defaults to null. + */ + minExtent: null, + + /** + * APIProperty: maxResolution + * {Float} Default max is 360 deg / 256 px, which corresponds to + * zoom level 0 on gmaps. Specify a different value in the layer + * options if you are not using the default <OpenLayers.Map.tileSize> + * and displaying the whole world. + */ + maxResolution: null, + + /** + * APIProperty: minResolution + * {Float} + */ + minResolution: null, + + /** + * APIProperty: numZoomLevels + * {Integer} + */ + numZoomLevels: null, + + /** + * APIProperty: minScale + * {Float} + */ + minScale: null, + + /** + * APIProperty: maxScale + * {Float} + */ + maxScale: null, + + /** + * APIProperty: displayOutsideMaxExtent + * {Boolean} Request map tiles that are completely outside of the max + * extent for this layer. Defaults to false. + */ + displayOutsideMaxExtent: false, + + /** + * APIProperty: wrapDateLine + * {Boolean} Wraps the world at the international dateline, so the map can + * be panned infinitely in longitudinal direction. Only use this on the + * base layer, and only if the layer's maxExtent equals the world bounds. + * #487 for more info. + */ + wrapDateLine: false, + + /** + * Property: metadata + * {Object} This object can be used to store additional information on a + * layer object. + */ + metadata: null, + + /** + * Constructor: OpenLayers.Layer + * + * Parameters: + * name - {String} The layer name + * options - {Object} Hashtable of extra options to tag onto the layer + */ + initialize: function(name, options) { + + this.metadata = {}; + + options = OpenLayers.Util.extend({}, options); + // make sure we respect alwaysInRange if set on the prototype + if (this.alwaysInRange != null) { + options.alwaysInRange = this.alwaysInRange; + } + this.addOptions(options); + + this.name = name; + + if (this.id == null) { + + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + + this.div = OpenLayers.Util.createDiv(this.id); + this.div.style.width = "100%"; + this.div.style.height = "100%"; + this.div.dir = "ltr"; + + this.events = new OpenLayers.Events(this, this.div); + if(this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + + } + }, + + /** + * Method: destroy + * Destroy is a destructor: this is to alleviate cyclic references which + * the Javascript garbage cleaner can not take care of on its own. + * + * Parameters: + * setNewBaseLayer - {Boolean} Set a new base layer when this layer has + * been destroyed. Default is true. + */ + destroy: function(setNewBaseLayer) { + if (setNewBaseLayer == null) { + setNewBaseLayer = true; + } + if (this.map != null) { + this.map.removeLayer(this, setNewBaseLayer); + } + this.projection = null; + this.map = null; + this.name = null; + this.div = null; + this.options = null; + + if (this.events) { + if(this.eventListeners) { + this.events.un(this.eventListeners); + } + this.events.destroy(); + } + this.eventListeners = null; + this.events = null; + }, + + /** + * Method: clone + * + * Parameters: + * obj - {<OpenLayers.Layer>} The layer to be cloned + * + * Returns: + * {<OpenLayers.Layer>} An exact clone of this <OpenLayers.Layer> + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer(this.name, this.getOptions()); + } + + // catch any randomly tagged-on properties + OpenLayers.Util.applyDefaults(obj, this); + + // a cloned layer should never have its map property set + // because it has not been added to a map yet. + obj.map = null; + + return obj; + }, + + /** + * Method: getOptions + * Extracts an object from the layer with the properties that were set as + * options, but updates them with the values currently set on the + * instance. + * + * Returns: + * {Object} the <options> of the layer, representing the current state. + */ + getOptions: function() { + var options = {}; + for(var o in this.options) { + options[o] = this[o]; + } + return options; + }, + + /** + * APIMethod: setName + * Sets the new layer name for this layer. Can trigger a changelayer event + * on the map. + * + * Parameters: + * newName - {String} The new name. + */ + setName: function(newName) { + if (newName != this.name) { + this.name = newName; + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "name" + }); + } + } + }, + + /** + * APIMethod: addOptions + * + * Parameters: + * newOptions - {Object} + * reinitialize - {Boolean} If set to true, and if resolution options of the + * current baseLayer were changed, the map will be recentered to make + * sure that it is displayed with a valid resolution, and a + * changebaselayer event will be triggered. + */ + addOptions: function (newOptions, reinitialize) { + + if (this.options == null) { + this.options = {}; + } + + if (newOptions) { + // make sure this.projection references a projection object + if(typeof newOptions.projection == "string") { + newOptions.projection = new OpenLayers.Projection(newOptions.projection); + } + if (newOptions.projection) { + // get maxResolution, units and maxExtent from projection defaults if + // they are not defined already + OpenLayers.Util.applyDefaults(newOptions, + OpenLayers.Projection.defaults[newOptions.projection.getCode()]); + } + // allow array for extents + if (newOptions.maxExtent && !(newOptions.maxExtent instanceof OpenLayers.Bounds)) { + newOptions.maxExtent = new OpenLayers.Bounds(newOptions.maxExtent); + } + if (newOptions.minExtent && !(newOptions.minExtent instanceof OpenLayers.Bounds)) { + newOptions.minExtent = new OpenLayers.Bounds(newOptions.minExtent); + } + } + + // update our copy for clone + OpenLayers.Util.extend(this.options, newOptions); + + // add new options to this + OpenLayers.Util.extend(this, newOptions); + + // get the units from the projection, if we have a projection + // and it it has units + if(this.projection && this.projection.getUnits()) { + this.units = this.projection.getUnits(); + } + + // re-initialize resolutions if necessary, i.e. if any of the + // properties of the "properties" array defined below is set + // in the new options + if(this.map) { + // store current resolution so we can try to restore it later + var resolution = this.map.getResolution(); + var properties = this.RESOLUTION_PROPERTIES.concat( + ["projection", "units", "minExtent", "maxExtent"] + ); + for(var o in newOptions) { + if(newOptions.hasOwnProperty(o) && + OpenLayers.Util.indexOf(properties, o) >= 0) { + + this.initResolutions(); + if (reinitialize && this.map.baseLayer === this) { + // update map position, and restore previous resolution + this.map.setCenter(this.map.getCenter(), + this.map.getZoomForResolution(resolution), + false, true + ); + // trigger a changebaselayer event to make sure that + // all controls (especially + // OpenLayers.Control.PanZoomBar) get notified of the + // new options + this.map.events.triggerEvent("changebaselayer", { + layer: this + }); + } + break; + } + } + } + }, + + /** + * APIMethod: onMapResize + * This function can be implemented by subclasses + */ + onMapResize: function() { + //this function can be implemented by subclasses + }, + + /** + * APIMethod: redraw + * Redraws the layer. Returns true if the layer was redrawn, false if not. + * + * Returns: + * {Boolean} The layer was redrawn. + */ + redraw: function() { + var redrawn = false; + if (this.map) { + + // min/max Range may have changed + this.inRange = this.calculateInRange(); + + // map's center might not yet be set + var extent = this.getExtent(); + + if (extent && this.inRange && this.visibility) { + var zoomChanged = true; + this.moveTo(extent, zoomChanged, false); + this.events.triggerEvent("moveend", + {"zoomChanged": zoomChanged}); + redrawn = true; + } + } + return redrawn; + }, + + /** + * Method: moveTo + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * zoomChanged - {Boolean} Tells when zoom has changed, as layers have to + * do some init work in that case. + * dragging - {Boolean} + */ + moveTo:function(bounds, zoomChanged, dragging) { + var display = this.visibility; + if (!this.isBaseLayer) { + display = display && this.inRange; + } + this.display(display); + }, + + /** + * Method: moveByPx + * Move the layer based on pixel vector. To be implemented by subclasses. + * + * Parameters: + * dx - {Number} The x coord of the displacement vector. + * dy - {Number} The y coord of the displacement vector. + */ + moveByPx: function(dx, dy) { + }, + + /** + * Method: setMap + * Set the map property for the layer. This is done through an accessor + * so that subclasses can override this and take special action once + * they have their map variable set. + * + * Here we take care to bring over any of the necessary default + * properties from the map. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + if (this.map == null) { + + this.map = map; + + // grab some essential layer data from the map if it hasn't already + // been set + this.maxExtent = this.maxExtent || this.map.maxExtent; + this.minExtent = this.minExtent || this.map.minExtent; + + this.projection = this.projection || this.map.projection; + if (typeof this.projection == "string") { + this.projection = new OpenLayers.Projection(this.projection); + } + + // Check the projection to see if we can get units -- if not, refer + // to properties. + this.units = this.projection.getUnits() || + this.units || this.map.units; + + this.initResolutions(); + + if (!this.isBaseLayer) { + this.inRange = this.calculateInRange(); + var show = ((this.visibility) && (this.inRange)); + this.div.style.display = show ? "" : "none"; + } + + // deal with gutters + this.setTileSize(); + } + }, + + /** + * Method: afterAdd + * Called at the end of the map.addLayer sequence. At this point, the map + * will have a base layer. To be overridden by subclasses. + */ + afterAdd: function() { + }, + + /** + * APIMethod: removeMap + * Just as setMap() allows each layer the possibility to take a + * personalized action on being added to the map, removeMap() allows + * each layer to take a personalized action on being removed from it. + * For now, this will be mostly unused, except for the EventPane layer, + * which needs this hook so that it can remove the special invisible + * pane. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + removeMap: function(map) { + //to be overridden by subclasses + }, + + /** + * APIMethod: getImageSize + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} optional tile bounds, can be used + * by subclasses that have to deal with different tile sizes at the + * layer extent edges (e.g. Zoomify) + * + * Returns: + * {<OpenLayers.Size>} The size that the image should be, taking into + * account gutters. + */ + getImageSize: function(bounds) { + return (this.imageSize || this.tileSize); + }, + + /** + * APIMethod: setTileSize + * Set the tile size based on the map size. This also sets layer.imageSize + * or use by Tile.Image. + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setTileSize: function(size) { + var tileSize = (size) ? size : + ((this.tileSize) ? this.tileSize : + this.map.getTileSize()); + this.tileSize = tileSize; + if(this.gutter) { + // layers with gutters need non-null tile sizes + //if(tileSize == null) { + // OpenLayers.console.error("Error in layer.setMap() for " + + // this.name + ": layers with " + + // "gutters need non-null tile sizes"); + //} + this.imageSize = new OpenLayers.Size(tileSize.w + (2*this.gutter), + tileSize.h + (2*this.gutter)); + } + }, + + /** + * APIMethod: getVisibility + * + * Returns: + * {Boolean} The layer should be displayed (if in range). + */ + getVisibility: function() { + return this.visibility; + }, + + /** + * APIMethod: setVisibility + * Set the visibility flag for the layer and hide/show & redraw + * accordingly. Fire event unless otherwise specified + * + * Note that visibility is no longer simply whether or not the layer's + * style.display is set to "block". Now we store a 'visibility' state + * property on the layer class, this allows us to remember whether or + * not we *desire* for a layer to be visible. In the case where the + * map's resolution is out of the layer's range, this desire may be + * subverted. + * + * Parameters: + * visibility - {Boolean} Whether or not to display the layer (if in range) + */ + setVisibility: function(visibility) { + if (visibility != this.visibility) { + this.visibility = visibility; + this.display(visibility); + this.redraw(); + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "visibility" + }); + } + this.events.triggerEvent("visibilitychanged"); + } + }, + + /** + * APIMethod: display + * Hide or show the Layer. This is designed to be used internally, and + * is not generally the way to enable or disable the layer. For that, + * use the setVisibility function instead.. + * + * Parameters: + * display - {Boolean} + */ + display: function(display) { + if (display != (this.div.style.display != "none")) { + this.div.style.display = (display && this.calculateInRange()) ? "block" : "none"; + } + }, + + /** + * APIMethod: calculateInRange + * + * Returns: + * {Boolean} The layer is displayable at the current map's current + * resolution. Note that if 'alwaysInRange' is true for the layer, + * this function will always return true. + */ + calculateInRange: function() { + var inRange = false; + + if (this.alwaysInRange) { + inRange = true; + } else { + if (this.map) { + var resolution = this.map.getResolution(); + inRange = ( (resolution >= this.minResolution) && + (resolution <= this.maxResolution) ); + } + } + return inRange; + }, + + /** + * APIMethod: setIsBaseLayer + * + * Parameters: + * isBaseLayer - {Boolean} + */ + setIsBaseLayer: function(isBaseLayer) { + if (isBaseLayer != this.isBaseLayer) { + this.isBaseLayer = isBaseLayer; + if (this.map != null) { + this.map.events.triggerEvent("changebaselayer", { + layer: this + }); + } + } + }, + + /********************************************************/ + /* */ + /* Baselayer Functions */ + /* */ + /********************************************************/ + + /** + * Method: initResolutions + * This method's responsibility is to set up the 'resolutions' array + * for the layer -- this array is what the layer will use to interface + * between the zoom levels of the map and the resolution display + * of the layer. + * + * The user has several options that determine how the array is set up. + * + * For a detailed explanation, see the following wiki from the + * openlayers.org homepage: + * http://trac.openlayers.org/wiki/SettingZoomLevels + */ + initResolutions: function() { + + // ok we want resolutions, here's our strategy: + // + // 1. if resolutions are defined in the layer config, use them + // 2. else, if scales are defined in the layer config then derive + // resolutions from these scales + // 3. else, attempt to calculate resolutions from maxResolution, + // minResolution, numZoomLevels, maxZoomLevel set in the + // layer config + // 4. if we still don't have resolutions, and if resolutions + // are defined in the same, use them + // 5. else, if scales are defined in the map then derive + // resolutions from these scales + // 6. else, attempt to calculate resolutions from maxResolution, + // minResolution, numZoomLevels, maxZoomLevel set in the + // map + // 7. hope for the best! + + var i, len, p; + var props = {}, alwaysInRange = true; + + // get resolution data from layer config + // (we also set alwaysInRange in the layer as appropriate) + for(i=0, len=this.RESOLUTION_PROPERTIES.length; i<len; i++) { + p = this.RESOLUTION_PROPERTIES[i]; + props[p] = this.options[p]; + if(alwaysInRange && this.options[p]) { + alwaysInRange = false; + } + } + if(this.options.alwaysInRange == null) { + this.alwaysInRange = alwaysInRange; + } + + // if we don't have resolutions then attempt to derive them from scales + if(props.resolutions == null) { + props.resolutions = this.resolutionsFromScales(props.scales); + } + + // if we still don't have resolutions then attempt to calculate them + if(props.resolutions == null) { + props.resolutions = this.calculateResolutions(props); + } + + // if we couldn't calculate resolutions then we look at we have + // in the map + if(props.resolutions == null) { + for(i=0, len=this.RESOLUTION_PROPERTIES.length; i<len; i++) { + p = this.RESOLUTION_PROPERTIES[i]; + props[p] = this.options[p] != null ? + this.options[p] : this.map[p]; + } + if(props.resolutions == null) { + props.resolutions = this.resolutionsFromScales(props.scales); + } + if(props.resolutions == null) { + props.resolutions = this.calculateResolutions(props); + } + } + + // ok, we new need to set properties in the instance + + // get maxResolution from the config if it's defined there + var maxResolution; + if(this.options.maxResolution && + this.options.maxResolution !== "auto") { + maxResolution = this.options.maxResolution; + } + if(this.options.minScale) { + maxResolution = OpenLayers.Util.getResolutionFromScale( + this.options.minScale, this.units); + } + + // get minResolution from the config if it's defined there + var minResolution; + if(this.options.minResolution && + this.options.minResolution !== "auto") { + minResolution = this.options.minResolution; + } + if(this.options.maxScale) { + minResolution = OpenLayers.Util.getResolutionFromScale( + this.options.maxScale, this.units); + } + + if(props.resolutions) { + + //sort resolutions array descendingly + props.resolutions.sort(function(a, b) { + return (b - a); + }); + + // if we still don't have a maxResolution get it from the + // resolutions array + if(!maxResolution) { + maxResolution = props.resolutions[0]; + } + + // if we still don't have a minResolution get it from the + // resolutions array + if(!minResolution) { + var lastIdx = props.resolutions.length - 1; + minResolution = props.resolutions[lastIdx]; + } + } + + this.resolutions = props.resolutions; + if(this.resolutions) { + len = this.resolutions.length; + this.scales = new Array(len); + for(i=0; i<len; i++) { + this.scales[i] = OpenLayers.Util.getScaleFromResolution( + this.resolutions[i], this.units); + } + this.numZoomLevels = len; + } + this.minResolution = minResolution; + if(minResolution) { + this.maxScale = OpenLayers.Util.getScaleFromResolution( + minResolution, this.units); + } + this.maxResolution = maxResolution; + if(maxResolution) { + this.minScale = OpenLayers.Util.getScaleFromResolution( + maxResolution, this.units); + } + }, + + /** + * Method: resolutionsFromScales + * Derive resolutions from scales. + * + * Parameters: + * scales - {Array(Number)} Scales + * + * Returns + * {Array(Number)} Resolutions + */ + resolutionsFromScales: function(scales) { + if(scales == null) { + return; + } + var resolutions, i, len; + len = scales.length; + resolutions = new Array(len); + for(i=0; i<len; i++) { + resolutions[i] = OpenLayers.Util.getResolutionFromScale( + scales[i], this.units); + } + return resolutions; + }, + + /** + * Method: calculateResolutions + * Calculate resolutions based on the provided properties. + * + * Parameters: + * props - {Object} Properties + * + * Returns: + * {Array({Number})} Array of resolutions. + */ + calculateResolutions: function(props) { + + var viewSize, wRes, hRes; + + // determine maxResolution + var maxResolution = props.maxResolution; + if(props.minScale != null) { + maxResolution = + OpenLayers.Util.getResolutionFromScale(props.minScale, + this.units); + } else if(maxResolution == "auto" && this.maxExtent != null) { + viewSize = this.map.getSize(); + wRes = this.maxExtent.getWidth() / viewSize.w; + hRes = this.maxExtent.getHeight() / viewSize.h; + maxResolution = Math.max(wRes, hRes); + } + + // determine minResolution + var minResolution = props.minResolution; + if(props.maxScale != null) { + minResolution = + OpenLayers.Util.getResolutionFromScale(props.maxScale, + this.units); + } else if(props.minResolution == "auto" && this.minExtent != null) { + viewSize = this.map.getSize(); + wRes = this.minExtent.getWidth() / viewSize.w; + hRes = this.minExtent.getHeight()/ viewSize.h; + minResolution = Math.max(wRes, hRes); + } + + if(typeof maxResolution !== "number" && + typeof minResolution !== "number" && + this.maxExtent != null) { + // maxResolution for default grid sets assumes that at zoom + // level zero, the whole world fits on one tile. + var tileSize = this.map.getTileSize(); + maxResolution = Math.max( + this.maxExtent.getWidth() / tileSize.w, + this.maxExtent.getHeight() / tileSize.h + ); + } + + // determine numZoomLevels + var maxZoomLevel = props.maxZoomLevel; + var numZoomLevels = props.numZoomLevels; + if(typeof minResolution === "number" && + typeof maxResolution === "number" && numZoomLevels === undefined) { + var ratio = maxResolution / minResolution; + numZoomLevels = Math.floor(Math.log(ratio) / Math.log(2)) + 1; + } else if(numZoomLevels === undefined && maxZoomLevel != null) { + numZoomLevels = maxZoomLevel + 1; + } + + // are we able to calculate resolutions? + if(typeof numZoomLevels !== "number" || numZoomLevels <= 0 || + (typeof maxResolution !== "number" && + typeof minResolution !== "number")) { + return; + } + + // now we have numZoomLevels and at least one of maxResolution + // or minResolution, we can populate the resolutions array + + var resolutions = new Array(numZoomLevels); + var base = 2; + if(typeof minResolution == "number" && + typeof maxResolution == "number") { + // if maxResolution and minResolution are set, we calculate + // the base for exponential scaling that starts at + // maxResolution and ends at minResolution in numZoomLevels + // steps. + base = Math.pow( + (maxResolution / minResolution), + (1 / (numZoomLevels - 1)) + ); + } + + var i; + if(typeof maxResolution === "number") { + for(i=0; i<numZoomLevels; i++) { + resolutions[i] = maxResolution / Math.pow(base, i); + } + } else { + for(i=0; i<numZoomLevels; i++) { + resolutions[numZoomLevels - 1 - i] = + minResolution * Math.pow(base, i); + } + } + + return resolutions; + }, + + /** + * APIMethod: getResolution + * + * Returns: + * {Float} The currently selected resolution of the map, taken from the + * resolutions array, indexed by current zoom level. + */ + getResolution: function() { + var zoom = this.map.getZoom(); + return this.getResolutionForZoom(zoom); + }, + + /** + * APIMethod: getExtent + * + * Returns: + * {<OpenLayers.Bounds>} A Bounds object which represents the lon/lat + * bounds of the current viewPort. + */ + getExtent: function() { + // just use stock map calculateBounds function -- passing no arguments + // means it will user map's current center & resolution + // + return this.map.calculateBounds(); + }, + + /** + * APIMethod: getZoomForExtent + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * closest - {Boolean} Find the zoom level that most closely fits the + * specified bounds. Note that this may result in a zoom that does + * not exactly contain the entire extent. + * Default is false. + * + * Returns: + * {Integer} The index of the zoomLevel (entry in the resolutions array) + * for the passed-in extent. We do this by calculating the ideal + * resolution for the given extent (based on the map size) and then + * calling getZoomForResolution(), passing along the 'closest' + * parameter. + */ + getZoomForExtent: function(extent, closest) { + var viewSize = this.map.getSize(); + var idealResolution = Math.max( extent.getWidth() / viewSize.w, + extent.getHeight() / viewSize.h ); + + return this.getZoomForResolution(idealResolution, closest); + }, + + /** + * Method: getDataExtent + * Calculates the max extent which includes all of the data for the layer. + * This function is to be implemented by subclasses. + * + * Returns: + * {<OpenLayers.Bounds>} + */ + getDataExtent: function () { + //to be implemented by subclasses + }, + + /** + * APIMethod: getResolutionForZoom + * + * Parameters: + * zoom - {Float} + * + * Returns: + * {Float} A suitable resolution for the specified zoom. + */ + getResolutionForZoom: function(zoom) { + zoom = Math.max(0, Math.min(zoom, this.resolutions.length - 1)); + var resolution; + if(this.map.fractionalZoom) { + var low = Math.floor(zoom); + var high = Math.ceil(zoom); + resolution = this.resolutions[low] - + ((zoom-low) * (this.resolutions[low]-this.resolutions[high])); + } else { + resolution = this.resolutions[Math.round(zoom)]; + } + return resolution; + }, + + /** + * APIMethod: getZoomForResolution + * + * Parameters: + * resolution - {Float} + * closest - {Boolean} Find the zoom level that corresponds to the absolute + * closest resolution, which may result in a zoom whose corresponding + * resolution is actually smaller than we would have desired (if this + * is being called from a getZoomForExtent() call, then this means that + * the returned zoom index might not actually contain the entire + * extent specified... but it'll be close). + * Default is false. + * + * Returns: + * {Integer} The index of the zoomLevel (entry in the resolutions array) + * that corresponds to the best fit resolution given the passed in + * value and the 'closest' specification. + */ + getZoomForResolution: function(resolution, closest) { + var zoom, i, len; + if(this.map.fractionalZoom) { + var lowZoom = 0; + var highZoom = this.resolutions.length - 1; + var highRes = this.resolutions[lowZoom]; + var lowRes = this.resolutions[highZoom]; + var res; + for(i=0, len=this.resolutions.length; i<len; ++i) { + res = this.resolutions[i]; + if(res >= resolution) { + highRes = res; + lowZoom = i; + } + if(res <= resolution) { + lowRes = res; + highZoom = i; + break; + } + } + var dRes = highRes - lowRes; + if(dRes > 0) { + zoom = lowZoom + ((highRes - resolution) / dRes); + } else { + zoom = lowZoom; + } + } else { + var diff; + var minDiff = Number.POSITIVE_INFINITY; + for(i=0, len=this.resolutions.length; i<len; i++) { + if (closest) { + diff = Math.abs(this.resolutions[i] - resolution); + if (diff > minDiff) { + break; + } + minDiff = diff; + } else { + if (this.resolutions[i] < resolution) { + break; + } + } + } + zoom = Math.max(0, i-1); + } + return zoom; + }, + + /** + * APIMethod: getLonLatFromViewPortPx + * + * Parameters: + * viewPortPx - {<OpenLayers.Pixel>|Object} An OpenLayers.Pixel or + * an object with a 'x' + * and 'y' properties. + * + * Returns: + * {<OpenLayers.LonLat>} An OpenLayers.LonLat which is the passed-in + * view port <OpenLayers.Pixel>, translated into lon/lat by the layer. + */ + getLonLatFromViewPortPx: function (viewPortPx) { + var lonlat = null; + var map = this.map; + if (viewPortPx != null && map.minPx) { + var res = map.getResolution(); + var maxExtent = map.getMaxExtent({restricted: true}); + var lon = (viewPortPx.x - map.minPx.x) * res + maxExtent.left; + var lat = (map.minPx.y - viewPortPx.y) * res + maxExtent.top; + lonlat = new OpenLayers.LonLat(lon, lat); + + if (this.wrapDateLine) { + lonlat = lonlat.wrapDateLine(this.maxExtent); + } + } + return lonlat; + }, + + /** + * APIMethod: getViewPortPxFromLonLat + * Returns a pixel location given a map location. This method will return + * fractional pixel values. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>|Object} An OpenLayers.LonLat or + * an object with a 'lon' + * and 'lat' properties. + * + * Returns: + * {<OpenLayers.Pixel>} An <OpenLayers.Pixel> which is the passed-in + * lonlat translated into view port pixels. + */ + getViewPortPxFromLonLat: function (lonlat, resolution) { + var px = null; + if (lonlat != null) { + resolution = resolution || this.map.getResolution(); + var extent = this.map.calculateBounds(null, resolution); + px = new OpenLayers.Pixel( + (1/resolution * (lonlat.lon - extent.left)), + (1/resolution * (extent.top - lonlat.lat)) + ); + } + return px; + }, + + /** + * APIMethod: setOpacity + * Sets the opacity for the entire layer (all images) + * + * Parameters: + * opacity - {Float} + */ + setOpacity: function(opacity) { + if (opacity != this.opacity) { + this.opacity = opacity; + var childNodes = this.div.childNodes; + for(var i = 0, len = childNodes.length; i < len; ++i) { + var element = childNodes[i].firstChild || childNodes[i]; + var lastChild = childNodes[i].lastChild; + //TODO de-uglify this + if (lastChild && lastChild.nodeName.toLowerCase() === "iframe") { + element = lastChild.parentNode; + } + OpenLayers.Util.modifyDOMElement(element, null, null, null, + null, null, null, opacity); + } + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "opacity" + }); + } + } + }, + + /** + * Method: getZIndex + * + * Returns: + * {Integer} the z-index of this layer + */ + getZIndex: function () { + return this.div.style.zIndex; + }, + + /** + * Method: setZIndex + * + * Parameters: + * zIndex - {Integer} + */ + setZIndex: function (zIndex) { + this.div.style.zIndex = zIndex; + }, + + /** + * Method: adjustBounds + * This function will take a bounds, and if wrapDateLine option is set + * on the layer, it will return a bounds which is wrapped around the + * world. We do not wrap for bounds which *cross* the + * maxExtent.left/right, only bounds which are entirely to the left + * or entirely to the right. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + */ + adjustBounds: function (bounds) { + + if (this.gutter) { + // Adjust the extent of a bounds in map units by the + // layer's gutter in pixels. + var mapGutter = this.gutter * this.map.getResolution(); + bounds = new OpenLayers.Bounds(bounds.left - mapGutter, + bounds.bottom - mapGutter, + bounds.right + mapGutter, + bounds.top + mapGutter); + } + + if (this.wrapDateLine) { + // wrap around the date line, within the limits of rounding error + var wrappingOptions = { + 'rightTolerance':this.getResolution(), + 'leftTolerance':this.getResolution() + }; + bounds = bounds.wrapDateLine(this.maxExtent, wrappingOptions); + + } + return bounds; + }, + + CLASS_NAME: "OpenLayers.Layer" +}); |