--- a/labs/openlayers/lib/OpenLayers/Map.js +++ b/labs/openlayers/lib/OpenLayers/Map.js @@ -1,1 +1,2418 @@ - +/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + +/** + * @requires OpenLayers/Util.js + * @requires OpenLayers/Events.js + * @requires OpenLayers/Tween.js + * @requires OpenLayers/Console.js + */ + +/** + * Class: OpenLayers.Map + * Instances of OpenLayers.Map are interactive maps embedded in a web page. + * Create a new map with the constructor. + * + * On their own maps do not provide much functionality. To extend a map + * it's necessary to add controls () and + * layers () to the map. + */ +OpenLayers.Map = OpenLayers.Class({ + + /** + * Constant: Z_INDEX_BASE + * {Object} Base z-indexes for different classes of thing + */ + Z_INDEX_BASE: { + BaseLayer: 100, + Overlay: 325, + Feature: 725, + Popup: 750, + Control: 1000 + }, + + /** + * Constant: EVENT_TYPES + * {Array(String)} Supported application event types. Register a listener + * for a particular event with the following syntax: + * (code) + * map.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 map.events.object. + * - *element* {DOMElement} A reference to map.events.element. + * + * Browser events have the following additional properties: + * - *xy* {} The pixel location of the event (relative + * to the the map viewport). + * - other properties that come with browser events + * + * Supported map event types: + * - *preaddlayer* triggered before a layer has been added. The event + * object will include a *layer* property that references the layer + * to be added. + * - *addlayer* triggered after a layer has been added. The event object + * will include a *layer* property that references the added layer. + * - *removelayer* triggered after a layer has been removed. The event + * object will include a *layer* property that references the removed + * layer. + * - *changelayer* triggered after a layer name change, order change, + * opacity change, params change or visibility change + * (due to resolution thresholds). Listeners will receive an event + * object with *layer* and *property* properties. The *layer* + * property will be a reference to the changed layer. + * The *property* property will be a key to the + * changed property (name, order, opacity, params or visibility). + * - *movestart* triggered after the start of a drag, pan, or zoom + * - *move* triggered after each drag, pan, or zoom + * - *moveend* triggered after a drag, pan, or zoom completes + * - *zoomend* triggered after a zoom completes + * - *mouseover* triggered after mouseover the map + * - *mouseout* triggered after mouseout the map + * - *mousemove* triggered after mousemove the map + * - *changebaselayer* triggered after the base layer changes + */ + EVENT_TYPES: [ + "preaddlayer", "addlayer", "removelayer", "changelayer", "movestart", + "move", "moveend", "zoomend", "popupopen", "popupclose", + "addmarker", "removemarker", "clearmarkers", "mouseover", + "mouseout", "mousemove", "dragstart", "drag", "dragend", + "changebaselayer"], + + /** + * Property: id + * {String} Unique identifier for the map + */ + id: null, + + /** + * Property: fractionalZoom + * {Boolean} For a base layer that supports it, allow the map resolution + * to be set to a value between one of the values in the resolutions + * array. Default is false. + * + * When fractionalZoom is set to true, it is possible to zoom to + * an arbitrary extent. This requires a base layer from a source + * that supports requests for arbitrary extents (i.e. not cached + * tiles on a regular lattice). This means that fractionalZoom + * will not work with commercial layers (Google, Yahoo, VE), layers + * using TileCache, or any other pre-cached data sources. + * + * If you are using fractionalZoom, then you should also use + * instead of layer.resolutions[zoom] as the + * former works for non-integer zoom levels. + */ + fractionalZoom: false, + + /** + * APIProperty: events + * {} An events object that handles all + * events on the map + */ + events: null, + + /** + * APIProperty: allOverlays + * {Boolean} Allow the map to function with "overlays" only. Defaults to + * false. If true, the lowest layer in the draw order will act as + * the base layer. In addition, if set to true, all layers will + * have isBaseLayer set to false when they are added to the map. + * + * Note: + * If you set map.allOverlays to true, then you *cannot* use + * map.setBaseLayer or layer.setIsBaseLayer. With allOverlays true, + * the lowest layer in the draw layer is the base layer. So, to change + * the base layer, use or to set the layer + * index to 0. + */ + allOverlays: false, + + /** + * APIProperty: div + * {DOMElement|String} The element that contains the map (or an id for + * that element). If the constructor is called + * with two arguments, this should be provided as the first argument. + * Alternatively, the map constructor can be called with the options + * object as the only argument. In this case (one argument), a + * div property may or may not be provided. If the div property + * is not provided, the map can be rendered to a container later + * using the method. + * + * Note: + * If you are calling after map construction, do not use + * auto. Instead, divide your by your + * maximum expected dimension. + */ + div: null, + + /** + * Property: dragging + * {Boolean} The map is currently being dragged. + */ + dragging: false, + + /** + * Property: size + * {} Size of the main div (this.div) + */ + size: null, + + /** + * Property: viewPortDiv + * {HTMLDivElement} The element that represents the map viewport + */ + viewPortDiv: null, + + /** + * Property: layerContainerOrigin + * {} The lonlat at which the later container was + * re-initialized (on-zoom) + */ + layerContainerOrigin: null, + + /** + * Property: layerContainerDiv + * {HTMLDivElement} The element that contains the layers. + */ + layerContainerDiv: null, + + /** + * APIProperty: layers + * {Array()} Ordered list of layers in the map + */ + layers: null, + + /** + * Property: controls + * {Array()} List of controls associated with the map. + * + * If not provided in the map options at construction, the map will + * be given the following controls by default: + * - + * - + * - + * - + */ + controls: null, + + /** + * Property: popups + * {Array()} List of popups associated with the map + */ + popups: null, + + /** + * APIProperty: baseLayer + * {} The currently selected base layer. This determines + * min/max zoom level, projection, etc. + */ + baseLayer: null, + + /** + * Property: center + * {} The current center of the map + */ + center: null, + + /** + * Property: resolution + * {Float} The resolution of the map. + */ + resolution: null, + + /** + * Property: zoom + * {Integer} The current zoom level of the map + */ + zoom: 0, + + /** + * Property: panRatio + * {Float} The ratio of the current extent within + * which panning will tween. + */ + panRatio: 1.5, + + /** + * Property: viewRequestID + * {String} Used to store a unique identifier that changes when the map + * view changes. viewRequestID should be used when adding data + * asynchronously to the map: viewRequestID is incremented when + * you initiate your request (right now during changing of + * baselayers and changing of zooms). It is stored here in the + * map and also in the data that will be coming back + * asynchronously. Before displaying this data on request + * completion, we check that the viewRequestID of the data is + * still the same as that of the map. Fix for #480 + */ + viewRequestID: 0, + + // Options + + /** + * APIProperty: tileSize + * {} Set in the map options to override the default tile + * size for this map. + */ + tileSize: null, + + /** + * APIProperty: projection + * {String} Set in the map options to override the default projection + * string this map - also set maxExtent, maxResolution, and + * units if appropriate. Default is "EPSG:4326". + */ + projection: "EPSG:4326", + + /** + * APIProperty: units + * {String} The map units. Defaults to 'degrees'. Possible values are + * 'degrees' (or 'dd'), 'm', 'ft', 'km', 'mi', 'inches'. + */ + units: 'degrees', + + /** + * APIProperty: resolutions + * {Array(Float)} 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: maxResolution + * {Float} Default max is 360 deg / 256 px, which corresponds to + * zoom level 0 on gmaps. Specify a different value in the map + * options if you are not using a geographic projection and + * displaying the whole world. + */ + maxResolution: 1.40625, + + /** + * APIProperty: minResolution + * {Float} + */ + minResolution: null, + + /** + * APIProperty: maxScale + * {Float} + */ + maxScale: null, + + /** + * APIProperty: minScale + * {Float} + */ + minScale: null, + + /** + * APIProperty: maxExtent + * {} The maximum extent for the map. Defaults to the + * whole world in decimal degrees + * (-180, -90, 180, 90). Specify a different + * extent in the map options if you are not using a + * geographic projection and displaying the whole + * world. + */ + maxExtent: null, + + /** + * APIProperty: minExtent + * {} + */ + minExtent: null, + + /** + * APIProperty: restrictedExtent + * {} Limit map navigation to this extent where possible. + * If a non-null restrictedExtent is set, panning will be restricted + * to the given bounds. In addition, zooming to a resolution that + * displays more than the restricted extent will center the map + * on the restricted extent. If you wish to limit the zoom level + * or resolution, use maxResolution. + */ + restrictedExtent: null, + + /** + * APIProperty: numZoomLevels + * {Integer} Number of zoom levels for the map. Defaults to 16. Set a + * different value in the map options if needed. + */ + numZoomLevels: 16, + + /** + * APIProperty: theme + * {String} Relative path to a CSS file from which to load theme styles. + * Specify null in the map options (e.g. {theme: null}) if you + * want to get cascading style declarations - by putting links to + * stylesheets or style declarations directly in your page. + */ + theme: null, + + /** + * APIProperty: displayProjection + * {} Requires proj4js support.Projection used by + * several controls to display data to user. If this property is set, + * it will be set on any control which has a null displayProjection + * property at the time the control is added to the map. + */ + displayProjection: null, + + /** + * APIProperty: fallThrough + * {Boolean} Should OpenLayers allow events on the map to fall through to + * other elements on the page, or should it swallow them? (#457) + * Default is to fall through. + */ + fallThrough: true, + + /** + * Property: panTween + * {OpenLayers.Tween} Animated panning tween object, see panTo() + */ + panTween: null, + + /** + * APIProperty: eventListeners + * {Object} If set as an option at construction, the eventListeners + * object will be registered with . Object + * structure must be a listeners object as shown in the example for + * the events.on method. + */ + eventListeners: null, + + /** + * APIProperty: panMethod + * {Function} The Easing function to be used for tweening. Default is + * OpenLayers.Easing.Expo.easeOut. Setting this to 'null' turns off + * animated panning. + */ + panMethod: OpenLayers.Easing.Expo.easeOut, + + /** + * Property: panDuration + * {Integer} The number of steps to be passed to the + * OpenLayers.Tween.start() method when the map is + * panned. + * Default is 50. + */ + panDuration: 50, + + /** + * Property: paddingForPopups + * {} Outside margin of the popup. Used to prevent + * the popup from getting too close to the map border. + */ + paddingForPopups : null, + + /** + * Constructor: OpenLayers.Map + * Constructor for a new OpenLayers.Map instance. There are two possible + * ways to call the map constructor. See the examples below. + * + * Parameters: + * div - {DOMElement|String} The element or id of an element in your page + * that will contain the map. May be omitted if the
option is + * provided or if you intend to call the method later. + * options - {Object} Optional object with properties to tag onto the map. + * + * Examples (method one): + * (code) + * // create a map with default options in an element with the id "map1" + * var map = new OpenLayers.Map("map1"); + * + * // create a map with non-default options in an element with id "map2" + * var options = { + * maxExtent: new OpenLayers.Bounds(-200000, -200000, 200000, 200000), + * maxResolution: 156543, + * units: 'm', + * projection: "EPSG:41001" + * }; + * var map = new OpenLayers.Map("map2", options); + * (end) + * + * Examples (method two - single argument): + * (code) + * // create a map with non-default options + * var map = new OpenLayers.Map({ + * div: "map_id", + * maxExtent: new OpenLayers.Bounds(-200000, -200000, 200000, 200000), + * maxResolution: 156543, + * units: 'm', + * projection: "EPSG:41001" + * }); + * + * // create a map without a reference to a container - call render later + * var map = new OpenLayers.Map({ + * maxExtent: new OpenLayers.Bounds(-200000, -200000, 200000, 200000), + * maxResolution: 156543, + * units: 'm', + * projection: "EPSG:41001" + * }); + */ + initialize: function (div, options) { + + // If only one argument is provided, check if it is an object. + if(arguments.length === 1 && typeof div === "object") { + options = div; + div = options && options.div; + } + + // Simple-type defaults are set in class definition. + // Now set complex-type defaults + this.tileSize = new OpenLayers.Size(OpenLayers.Map.TILE_WIDTH, + OpenLayers.Map.TILE_HEIGHT); + + this.maxExtent = new OpenLayers.Bounds(-180, -90, 180, 90); + + this.paddingForPopups = new OpenLayers.Bounds(15, 15, 15, 15); + + this.theme = OpenLayers._getScriptLocation() + + 'theme/default/style.css'; + + // now override default options + OpenLayers.Util.extend(this, options); + + // initialize layers array + this.layers = []; + + this.id = OpenLayers.Util.createUniqueID("OpenLayers.Map_"); + + this.div = OpenLayers.Util.getElement(div); + if(!this.div) { + this.div = document.createElement("div"); + this.div.style.height = "1px"; + this.div.style.width = "1px"; + } + + OpenLayers.Element.addClass(this.div, 'olMap'); + + // the viewPortDiv is the outermost div we modify + var id = this.id + "_OpenLayers_ViewPort"; + this.viewPortDiv = OpenLayers.Util.createDiv(id, null, null, null, + "relative", null, + "hidden"); + this.viewPortDiv.style.width = "100%"; + this.viewPortDiv.style.height = "100%"; + this.viewPortDiv.className = "olMapViewport"; + this.div.appendChild(this.viewPortDiv); + + // the layerContainerDiv is the one that holds all the layers + id = this.id + "_OpenLayers_Container"; + this.layerContainerDiv = OpenLayers.Util.createDiv(id); + this.layerContainerDiv.style.zIndex=this.Z_INDEX_BASE['Popup']-1; + + this.viewPortDiv.appendChild(this.layerContainerDiv); + + this.events = new OpenLayers.Events(this, + this.div, + this.EVENT_TYPES, + this.fallThrough, + {includeXY: true}); + this.updateSize(); + if(this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + + // update the map size and location before the map moves + this.events.register("movestart", this, this.updateSize); + + // Because Mozilla does not support the "resize" event for elements + // other than "window", we need to put a hack here. + if (OpenLayers.String.contains(navigator.appName, "Microsoft")) { + // If IE, register the resize on the div + this.events.register("resize", this, this.updateSize); + } else { + // Else updateSize on catching the window's resize + // Note that this is ok, as updateSize() does nothing if the + // map's size has not actually changed. + this.updateSizeDestroy = OpenLayers.Function.bind(this.updateSize, + this); + OpenLayers.Event.observe(window, 'resize', + this.updateSizeDestroy); + } + + // only append link stylesheet if the theme property is set + if(this.theme) { + // check existing links for equivalent url + var addNode = true; + var nodes = document.getElementsByTagName('link'); + for(var i=0, len=nodes.length; i=0; --i) { + this.controls[i].destroy(); + } + this.controls = null; + } + if (this.layers != null) { + for (var i = this.layers.length - 1; i>=0; --i) { + //pass 'false' to destroy so that map wont try to set a new + // baselayer after each baselayer is removed + this.layers[i].destroy(false); + } + this.layers = null; + } + if (this.viewPortDiv) { + this.div.removeChild(this.viewPortDiv); + } + this.viewPortDiv = null; + + if(this.eventListeners) { + this.events.un(this.eventListeners); + this.eventListeners = null; + } + this.events.destroy(); + this.events = null; + + }, + + /** + * APIMethod: setOptions + * Change the map options + * + * Parameters: + * options - {Object} Hashtable of options to tag to the map + */ + setOptions: function(options) { + OpenLayers.Util.extend(this, options); + }, + + /** + * APIMethod: getTileSize + * Get the tile size for the map + * + * Returns: + * {} + */ + getTileSize: function() { + return this.tileSize; + }, + + + /** + * APIMethod: getBy + * Get a list of objects given a property and a match item. + * + * Parameters: + * array - {String} A property on the map whose value is an array. + * property - {String} A property on each item of the given array. + * match - {String | Object} A string to match. Can also be a regular + * expression literal or object. In addition, it can be any object + * with a method named test. For reqular expressions or other, if + * match.test(map[array][i][property]) evaluates to true, the item will + * be included in the array returned. If no items are found, an empty + * array is returned. + * + * Returns: + * {Array} An array of items where the given property matches the given + * criteria. + */ + getBy: function(array, property, match) { + var test = (typeof match.test == "function"); + var found = OpenLayers.Array.filter(this[array], function(item) { + return item[property] == match || (test && match.test(item[property])); + }); + return found; + }, + + /** + * APIMethod: getLayersBy + * Get a list of layers with properties matching the given criteria. + * + * Parameter: + * property - {String} A layer property to be matched. + * match - {String | Object} A string to match. Can also be a regular + * expression literal or object. In addition, it can be any object + * with a method named test. For reqular expressions or other, if + * match.test(layer[property]) evaluates to true, the layer will be + * included in the array returned. If no layers are found, an empty + * array is returned. + * + * Returns: + * {Array()} A list of layers matching the given criteria. + * An empty array is returned if no matches are found. + */ + getLayersBy: function(property, match) { + return this.getBy("layers", property, match); + }, + + /** + * APIMethod: getLayersByName + * Get a list of layers with names matching the given name. + * + * Parameter: + * match - {String | Object} A layer name. The name can also be a regular + * expression literal or object. In addition, it can be any object + * with a method named test. For reqular expressions or other, if + * name.test(layer.name) evaluates to true, the layer will be included + * in the list of layers returned. If no layers are found, an empty + * array is returned. + * + * Returns: + * {Array()} A list of layers matching the given name. + * An empty array is returned if no matches are found. + */ + getLayersByName: function(match) { + return this.getLayersBy("name", match); + }, + + /** + * APIMethod: getLayersByClass + * Get a list of layers of a given class (CLASS_NAME). + * + * Parameter: + * match - {String | Object} A layer class name. The match can also be a + * regular expression literal or object. In addition, it can be any + * object with a method named test. For reqular expressions or other, + * if type.test(layer.CLASS_NAME) evaluates to true, the layer will + * be included in the list of layers returned. If no layers are + * found, an empty array is returned. + * + * Returns: + * {Array()} A list of layers matching the given class. + * An empty array is returned if no matches are found. + */ + getLayersByClass: function(match) { + return this.getLayersBy("CLASS_NAME", match); + }, + + /** + * APIMethod: getControlsBy + * Get a list of controls with properties matching the given criteria. + * + * Parameter: + * property - {String} A control property to be matched. + * match - {String | Object} A string to match. Can also be a regular + * expression literal or object. In addition, it can be any object + * with a method named test. For reqular expressions or other, if + * match.test(layer[property]) evaluates to true, the layer will be + * included in the array returned. If no layers are found, an empty + * array is returned. + * + * Returns: + * {Array()} A list of controls matching the given + * criteria. An empty array is returned if no matches are found. + */ + getControlsBy: function(property, match) { + return this.getBy("controls", property, match); + }, + + /** + * APIMethod: getControlsByClass + * Get a list of controls of a given class (CLASS_NAME). + * + * Parameter: + * match - {String | Object} A control class name. The match can also be a + * regular expression literal or object. In addition, it can be any + * object with a method named test. For reqular expressions or other, + * if type.test(control.CLASS_NAME) evaluates to true, the control will + * be included in the list of controls returned. If no controls are + * found, an empty array is returned. + * + * Returns: + * {Array()} A list of controls matching the given class. + * An empty array is returned if no matches are found. + */ + getControlsByClass: function(match) { + return this.getControlsBy("CLASS_NAME", match); + }, + + /********************************************************/ + /* */ + /* Layer Functions */ + /* */ + /* The following functions deal with adding and */ + /* removing Layers to and from the Map */ + /* */ + /********************************************************/ + + /** + * APIMethod: getLayer + * Get a layer based on its id + * + * Parameter: + * id - {String} A layer id + * + * Returns: + * {} The Layer with the corresponding id from the map's + * layer collection, or null if not found. + */ + getLayer: function(id) { + var foundLayer = null; + for (var i=0, len=this.layers.length; i} + * zIdx - {int} + */ + setLayerZIndex: function (layer, zIdx) { + layer.setZIndex( + this.Z_INDEX_BASE[layer.isBaseLayer ? 'BaseLayer' : 'Overlay'] + + zIdx * 5 ); + }, + + /** + * Method: resetLayersZIndex + * Reset each layer's z-index based on layer's array index + */ + resetLayersZIndex: function() { + for (var i=0, len=this.layers.length; i} + */ + addLayer: function (layer) { + for(var i=0, len=this.layers.length; i )} + */ + addLayers: function (layers) { + for (var i=0, len=layers.length; i} + * setNewBaseLayer - {Boolean} Default is true + */ + removeLayer: function(layer, setNewBaseLayer) { + if (setNewBaseLayer == null) { + setNewBaseLayer = true; + } + + if (layer.isFixed) { + this.viewPortDiv.removeChild(layer.div); + } else { + this.layerContainerDiv.removeChild(layer.div); + } + OpenLayers.Util.removeItem(this.layers, layer); + layer.removeMap(this); + layer.map = null; + + // if we removed the base layer, need to set a new one + if(this.baseLayer == layer) { + this.baseLayer = null; + if(setNewBaseLayer) { + for(var i=0, len=this.layers.length; i} + * + * Returns: + * {Integer} The current (zero-based) index of the given layer in the map's + * layer stack. Returns -1 if the layer isn't on the map. + */ + getLayerIndex: function (layer) { + return OpenLayers.Util.indexOf(this.layers, layer); + }, + + /** + * APIMethod: setLayerIndex + * Move the given layer to the specified (zero-based) index in the layer + * list, changing its z-index in the map display. Use + * map.getLayerIndex() to find out the current index of a layer. Note + * that this cannot (or at least should not) be effectively used to + * raise base layers above overlays. + * + * Parameters: + * layer - {} + * idx - {int} + */ + setLayerIndex: function (layer, idx) { + var base = this.getLayerIndex(layer); + if (idx < 0) { + idx = 0; + } else if (idx > this.layers.length) { + idx = this.layers.length; + } + if (base != idx) { + this.layers.splice(base, 1); + this.layers.splice(idx, 0, layer); + for (var i=0, len=this.layers.length; i} + * delta - {int} + */ + raiseLayer: function (layer, delta) { + var idx = this.getLayerIndex(layer) + delta; + this.setLayerIndex(layer, idx); + }, + + /** + * APIMethod: setBaseLayer + * Allows user to specify one of the currently-loaded layers as the Map's + * new base layer. + * + * Parameters: + * newBaseLayer - {} + */ + setBaseLayer: function(newBaseLayer) { + + if (newBaseLayer != this.baseLayer) { + + // ensure newBaseLayer is already loaded + if (OpenLayers.Util.indexOf(this.layers, newBaseLayer) != -1) { + + // preserve center and scale when changing base layers + var center = this.getCenter(); + var newResolution = OpenLayers.Util.getResolutionFromScale( + this.getScale(), newBaseLayer.units + ); + + // make the old base layer invisible + if (this.baseLayer != null && !this.allOverlays) { + this.baseLayer.setVisibility(false); + } + + // set new baselayer + this.baseLayer = newBaseLayer; + + // Increment viewRequestID since the baseLayer is + // changing. This is used by tiles to check if they should + // draw themselves. + this.viewRequestID++; + if(!this.allOverlays || this.baseLayer.visibility) { + this.baseLayer.setVisibility(true); + } + + // recenter the map + if (center != null) { + // new zoom level derived from old scale + var newZoom = this.getZoomForResolution( + newResolution || this.resolution, true + ); + // zoom and force zoom change + this.setCenter(center, newZoom, false, true); + } + + this.events.triggerEvent("changebaselayer", { + layer: this.baseLayer + }); + } + } + }, + + + /********************************************************/ + /* */ + /* Control Functions */ + /* */ + /* The following functions deal with adding and */ + /* removing Controls to and from the Map */ + /* */ + /********************************************************/ + + /** + * APIMethod: addControl + * Add the passed over control to the map. Optionally + * position the control at the given pixel. + * + * Parameters: + * control - {} + * px - {} + */ + addControl: function (control, px) { + this.controls.push(control); + this.addControlToMap(control, px); + }, + + /** + * APIMethod: addControls + * Add all of the passed over controls to the map. + * You can pass over an optional second array + * with pixel-objects to position the controls. + * The indices of the two arrays should match and + * you can add null as pixel for those controls + * you want to be autopositioned. + * + * Parameters: + * controls - {Array()} + * pixels - {Array()} + */ + addControls: function (controls, pixels) { + var pxs = (arguments.length === 1) ? [] : pixels; + for (var i=0, len=controls.length; i} + * px - {} + */ + addControlToMap: function (control, px) { + // If a control doesn't have a div at this point, it belongs in the + // viewport. + control.outsideViewport = (control.div != null); + + // If the map has a displayProjection, and the control doesn't, set + // the display projection. + if (this.displayProjection && !control.displayProjection) { + control.displayProjection = this.displayProjection; + } + + control.setMap(this); + var div = control.draw(px); + if (div) { + if(!control.outsideViewport) { + div.style.zIndex = this.Z_INDEX_BASE['Control'] + + this.controls.length; + this.viewPortDiv.appendChild( div ); + } + } + if(control.autoActivate) { + control.activate(); + } + }, + + /** + * APIMethod: getControl + * + * Parameters: + * id - {String} ID of the control to return. + * + * Returns: + * {} The control from the map's list of controls + * which has a matching 'id'. If none found, + * returns null. + */ + getControl: function (id) { + var returnControl = null; + for(var i=0, len=this.controls.length; i} The control to remove. + */ + removeControl: function (control) { + //make sure control is non-null and actually part of our map + if ( (control) && (control == this.getControl(control.id)) ) { + if (control.div && (control.div.parentNode == this.viewPortDiv)) { + this.viewPortDiv.removeChild(control.div); + } + OpenLayers.Util.removeItem(this.controls, control); + } + }, + + /********************************************************/ + /* */ + /* Popup Functions */ + /* */ + /* The following functions deal with adding and */ + /* removing Popups to and from the Map */ + /* */ + /********************************************************/ + + /** + * APIMethod: addPopup + * + * Parameters: + * popup - {} + * exclusive - {Boolean} If true, closes all other popups first + */ + addPopup: function(popup, exclusive) { + + if (exclusive) { + //remove all other popups from screen + for (var i = this.popups.length - 1; i >= 0; --i) { + this.removePopup(this.popups[i]); + } + } + + popup.map = this; + this.popups.push(popup); + var popupDiv = popup.draw(); + if (popupDiv) { + popupDiv.style.zIndex = this.Z_INDEX_BASE['Popup'] + + this.popups.length; + this.layerContainerDiv.appendChild(popupDiv); + } + }, + + /** + * APIMethod: removePopup + * + * Parameters: + * popup - {} + */ + removePopup: function(popup) { + OpenLayers.Util.removeItem(this.popups, popup); + if (popup.div) { + try { this.layerContainerDiv.removeChild(popup.div); } + catch (e) { } // Popups sometimes apparently get disconnected + // from the layerContainerDiv, and cause complaints. + } + popup.map = null; + }, + + /********************************************************/ + /* */ + /* Container Div Functions */ + /* */ + /* The following functions deal with the access to */ + /* and maintenance of the size of the container div */ + /* */ + /********************************************************/ + + /** + * APIMethod: getSize + * + * Returns: + * {} An object that represents the + * size, in pixels, of the div into which OpenLayers + * has been loaded. + * Note - A clone() of this locally cached variable is + * returned, so as not to allow users to modify it. + */ + getSize: function () { + var size = null; + if (this.size != null) { + size = this.size.clone(); + } + return size; + }, + + /** + * APIMethod: updateSize + * This function should be called by any external code which dynamically + * changes the size of the map div (because mozilla wont let us catch + * the "onresize" for an element) + */ + updateSize: function() { + // the div might have moved on the page, also + var newSize = this.getCurrentSize(); + if (newSize && !isNaN(newSize.h) && !isNaN(newSize.w)) { + this.events.clearMouseCache(); + var oldSize = this.getSize(); + if (oldSize == null) { + this.size = oldSize = newSize; + } + if (!newSize.equals(oldSize)) { + + // store the new size + this.size = newSize; + + //notify layers of mapresize + for(var i=0, len=this.layers.length; i} A new object with the dimensions + * of the map div + */ + getCurrentSize: function() { + + var size = new OpenLayers.Size(this.div.clientWidth, + this.div.clientHeight); + + if (size.w == 0 && size.h == 0 || isNaN(size.w) && isNaN(size.h)) { + size.w = this.div.offsetWidth; + size.h = this.div.offsetHeight; + } + if (size.w == 0 && size.h == 0 || isNaN(size.w) && isNaN(size.h)) { + size.w = parseInt(this.div.style.width); + size.h = parseInt(this.div.style.height); + } + return size; + }, + + /** + * Method: calculateBounds + * + * Parameters: + * center - {} Default is this.getCenter() + * resolution - {float} Default is this.getResolution() + * + * Returns: + * {} A bounds based on resolution, center, and + * current mapsize. + */ + calculateBounds: function(center, resolution) { + + var extent = null; + + if (center == null) { + center = this.getCenter(); + } + if (resolution == null) { + resolution = this.getResolution(); + } + + if ((center != null) && (resolution != null)) { + + var size = this.getSize(); + var w_deg = size.w * resolution; + var h_deg = size.h * resolution; + + extent = new OpenLayers.Bounds(center.lon - w_deg / 2, + center.lat - h_deg / 2, + center.lon + w_deg / 2, + center.lat + h_deg / 2); + + } + + return extent; + }, + + + /********************************************************/ + /* */ + /* Zoom, Center, Pan Functions */ + /* */ + /* The following functions handle the validation, */ + /* getting and setting of the Zoom Level and Center */ + /* as well as the panning of the Map */ + /* */ + /********************************************************/ + /** + * APIMethod: getCenter + * + * Returns: + * {} + */ + getCenter: function () { + var center = null; + if (this.center) { + center = this.center.clone(); + } + return center; + }, + + + /** + * APIMethod: getZoom + * + * Returns: + * {Integer} + */ + getZoom: function () { + return this.zoom; + }, + + /** + * APIMethod: pan + * Allows user to pan by a value of screen pixels + * + * Parameters: + * dx - {Integer} + * dy - {Integer} + * options - {Object} Options to configure panning: + * - *animate* {Boolean} Use panTo instead of setCenter. Default is true. + * - *dragging* {Boolean} Call setCenter with dragging true. Default is + * false. + */ + pan: function(dx, dy, options) { + options = OpenLayers.Util.applyDefaults(options, { + animate: true, + dragging: false + }); + // getCenter + var centerPx = this.getViewPortPxFromLonLat(this.getCenter()); + + // adjust + var newCenterPx = centerPx.add(dx, dy); + + // only call setCenter if not dragging or there has been a change + if (!options.dragging || !newCenterPx.equals(centerPx)) { + var newCenterLonLat = this.getLonLatFromViewPortPx(newCenterPx); + if (options.animate) { + this.panTo(newCenterLonLat); + } else { + this.setCenter(newCenterLonLat, null, options.dragging); + } + } + + }, + + /** + * APIMethod: panTo + * Allows user to pan to a new lonlat + * If the new lonlat is in the current extent the map will slide smoothly + * + * Parameters: + * lonlat - {} + */ + panTo: function(lonlat) { + if (this.panMethod && this.getExtent().scale(this.panRatio).containsLonLat(lonlat)) { + if (!this.panTween) { + this.panTween = new OpenLayers.Tween(this.panMethod); + } + var center = this.getCenter(); + + // center will not change, don't do nothing + if (lonlat.lon == center.lon && + lonlat.lat == center.lat) { + return; + } + + var from = { + lon: center.lon, + lat: center.lat + }; + var to = { + lon: lonlat.lon, + lat: lonlat.lat + }; + this.panTween.start(from, to, this.panDuration, { + callbacks: { + start: OpenLayers.Function.bind(function(lonlat) { + this.events.triggerEvent("movestart"); + }, this), + eachStep: OpenLayers.Function.bind(function(lonlat) { + lonlat = new OpenLayers.LonLat(lonlat.lon, lonlat.lat); + this.moveTo(lonlat, this.zoom, { + 'dragging': true, + 'noEvent': true + }); + }, this), + done: OpenLayers.Function.bind(function(lonlat) { + lonlat = new OpenLayers.LonLat(lonlat.lon, lonlat.lat); + this.moveTo(lonlat, this.zoom, { + 'noEvent': true + }); + this.events.triggerEvent("moveend"); + }, this) + } + }); + } else { + this.setCenter(lonlat); + } + }, + + /** + * APIMethod: setCenter + * Set the map center (and optionally, the zoom level). + * + * Parameters: + * lonlat - {} The new center location. + * zoom - {Integer} Optional zoom level. + * dragging - {Boolean} Specifies whether or not to trigger + * movestart/end events + * forceZoomChange - {Boolean} Specifies whether or not to trigger zoom + * change events (needed on baseLayer change) + * + * TBD: reconsider forceZoomChange in 3.0 + */ + setCenter: function(lonlat, zoom, dragging, forceZoomChange) { + this.moveTo(lonlat, zoom, { + 'dragging': dragging, + 'forceZoomChange': forceZoomChange, + 'caller': 'setCenter' + }); + }, + + /** + * Method: moveTo + * + * Parameters: + * lonlat - {} + * zoom - {Integer} + * options - {Object} + */ + moveTo: function(lonlat, zoom, options) { + if (!options) { + options = {}; + } + if (zoom != null) { + zoom = parseFloat(zoom); + if (!this.fractionalZoom) { + zoom = Math.round(zoom); + } + } + // dragging is false by default + var dragging = options.dragging; + // forceZoomChange is false by default + var forceZoomChange = options.forceZoomChange; + // noEvent is false by default + var noEvent = options.noEvent; + + if (this.panTween && options.caller == "setCenter") { + this.panTween.stop(); + } + + if (!this.center && !this.isValidLonLat(lonlat)) { + lonlat = this.maxExtent.getCenterLonLat(); + } + + if(this.restrictedExtent != null) { + // In 3.0, decide if we want to change interpretation of maxExtent. + if(lonlat == null) { + lonlat = this.getCenter(); + } + if(zoom == null) { + zoom = this.getZoom(); + } + var resolution = this.getResolutionForZoom(zoom); + var extent = this.calculateBounds(lonlat, resolution); + if(!this.restrictedExtent.containsBounds(extent)) { + var maxCenter = this.restrictedExtent.getCenterLonLat(); + if(extent.getWidth() > this.restrictedExtent.getWidth()) { + lonlat = new OpenLayers.LonLat(maxCenter.lon, lonlat.lat); + } else if(extent.left < this.restrictedExtent.left) { + lonlat = lonlat.add(this.restrictedExtent.left - + extent.left, 0); + } else if(extent.right > this.restrictedExtent.right) { + lonlat = lonlat.add(this.restrictedExtent.right - + extent.right, 0); + } + if(extent.getHeight() > this.restrictedExtent.getHeight()) { + lonlat = new OpenLayers.LonLat(lonlat.lon, maxCenter.lat); + } else if(extent.bottom < this.restrictedExtent.bottom) { + lonlat = lonlat.add(0, this.restrictedExtent.bottom - + extent.bottom); + } + else if(extent.top > this.restrictedExtent.top) { + lonlat = lonlat.add(0, this.restrictedExtent.top - + extent.top); + } + } + } + + var zoomChanged = forceZoomChange || ( + (this.isValidZoomLevel(zoom)) && + (zoom != this.getZoom()) ); + + var centerChanged = (this.isValidLonLat(lonlat)) && + (!lonlat.equals(this.center)); + + + // if neither center nor zoom will change, no need to do anything + if (zoomChanged || centerChanged || !dragging) { + + if (!this.dragging && !noEvent) { + this.events.triggerEvent("movestart"); + } + + if (centerChanged) { + if ((!zoomChanged) && (this.center)) { + // if zoom hasnt changed, just slide layerContainer + // (must be done before setting this.center to new value) + this.centerLayerContainer(lonlat); + } + this.center = lonlat.clone(); + } + + // (re)set the layerContainerDiv's location + if ((zoomChanged) || (this.layerContainerOrigin == null)) { + this.layerContainerOrigin = this.center.clone(); + this.layerContainerDiv.style.left = "0px"; + this.layerContainerDiv.style.top = "0px"; + } + + if (zoomChanged) { + this.zoom = zoom; + this.resolution = this.getResolutionForZoom(zoom); + // zoom level has changed, increment viewRequestID. + this.viewRequestID++; + } + + var bounds = this.getExtent(); + + //send the move call to the baselayer and all the overlays + + if(this.baseLayer.visibility) { + this.baseLayer.moveTo(bounds, zoomChanged, dragging); + if(dragging) { + this.baseLayer.events.triggerEvent("move"); + } else { + this.baseLayer.events.triggerEvent("moveend", + {"zoomChanged": zoomChanged} + ); + } + } + + bounds = this.baseLayer.getExtent(); + + for (var i=0, len=this.layers.length; i} + */ + centerLayerContainer: function (lonlat) { + + var originPx = this.getViewPortPxFromLonLat(this.layerContainerOrigin); + var newPx = this.getViewPortPxFromLonLat(lonlat); + + if ((originPx != null) && (newPx != null)) { + this.layerContainerDiv.style.left = Math.round(originPx.x - newPx.x) + "px"; + this.