/* 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.layerContainerDiv.style.top = Math.round(originPx.y - newPx.y) + "px"; } }, /** * Method: isValidZoomLevel * * Parameters: * zoomLevel - {Integer} * * Returns: * {Boolean} Whether or not the zoom level passed in is non-null and * within the min/max range of zoom levels. */ isValidZoomLevel: function(zoomLevel) { return ( (zoomLevel != null) && (zoomLevel >= 0) && (zoomLevel < this.getNumZoomLevels()) ); }, /** * Method: isValidLonLat * * Parameters: * lonlat - {} * * Returns: * {Boolean} Whether or not the lonlat passed in is non-null and within * the maxExtent bounds */ isValidLonLat: function(lonlat) { var valid = false; if (lonlat != null) { var maxExtent = this.getMaxExtent(); valid = maxExtent.containsLonLat(lonlat); } return valid; }, /********************************************************/ /* */ /* Layer Options */ /* */ /* Accessor functions to Layer Options parameters */ /* */ /********************************************************/ /** * APIMethod: getProjection * This method returns a string representing the projection. In * the case of projection support, this will be the srsCode which * is loaded -- otherwise it will simply be the string value that * was passed to the projection at startup. * * FIXME: In 3.0, we will remove getProjectionObject, and instead * return a Projection object from this function. * * Returns: * {String} The Projection string from the base layer or null. */ getProjection: function() { var projection = this.getProjectionObject(); return projection ? projection.getCode() : null; }, /** * APIMethod: getProjectionObject * Returns the projection obect from the baselayer. * * Returns: * {} The Projection of the base layer. */ getProjectionObject: function() { var projection = null; if (this.baseLayer != null) { projection = this.baseLayer.projection; } return projection; }, /** * APIMethod: getMaxResolution * * Returns: * {String} The Map's Maximum Resolution */ getMaxResolution: function() { var maxResolution = null; if (this.baseLayer != null) { maxResolution = this.baseLayer.maxResolution; } return maxResolution; }, /** * APIMethod: getMaxExtent * * Parameters: * options - {Object} * * Allowed Options: * restricted - {Boolean} If true, returns restricted extent (if it is * available.) * * Returns: * {} The maxExtent property as set on the current * baselayer, unless the 'restricted' option is set, in which case * the 'restrictedExtent' option from the map is returned (if it * is set). */ getMaxExtent: function (options) { var maxExtent = null; if(options && options.restricted && this.restrictedExtent){ maxExtent = this.restrictedExtent; } else if (this.baseLayer != null) { maxExtent = this.baseLayer.maxExtent; } return maxExtent; }, /** * APIMethod: getNumZoomLevels * * Returns: * {Integer} The total number of zoom levels that can be displayed by the * current baseLayer. */ getNumZoomLevels: function() { var numZoomLevels = null; if (this.baseLayer != null) { numZoomLevels = this.baseLayer.numZoomLevels; } return numZoomLevels; }, /********************************************************/ /* */ /* Baselayer Functions */ /* */ /* The following functions, all publicly exposed */ /* in the API?, are all merely wrappers to the */ /* the same calls on whatever layer is set as */ /* the current base layer */ /* */ /********************************************************/ /** * APIMethod: getExtent * * Returns: * {} A Bounds object which represents the lon/lat * bounds of the current viewPort. * If no baselayer is set, returns null. */ getExtent: function () { var extent = null; if (this.baseLayer != null) { extent = this.baseLayer.getExtent(); } return extent; }, /** * APIMethod: getResolution * * Returns: * {Float} The current resolution of the map. * If no baselayer is set, returns null. */ getResolution: function () { var resolution = null; if (this.baseLayer != null) { resolution = this.baseLayer.getResolution(); } else if(this.allOverlays === true && this.layers.length > 0) { // while adding the 1st layer to the map in allOverlays mode, // this.baseLayer is not set yet when we need the resolution // for calculateInRange. resolution = this.layers[0].getResolution(); } return resolution; }, /** * APIMethod: getUnits * * Returns: * {Float} The current units of the map. * If no baselayer is set, returns null. */ getUnits: function () { var units = null; if (this.baseLayer != null) { units = this.baseLayer.units; } return units; }, /** * APIMethod: getScale * * Returns: * {Float} The current scale denominator of the map. * If no baselayer is set, returns null. */ getScale: function () { var scale = null; if (this.baseLayer != null) { var res = this.getResolution(); var units = this.baseLayer.units; scale = OpenLayers.Util.getScaleFromResolution(res, units); } return scale; }, /** * APIMethod: getZoomForExtent * * Parameters: * 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} A suitable zoom level for the specified bounds. * If no baselayer is set, returns null. */ getZoomForExtent: function (bounds, closest) { var zoom = null; if (this.baseLayer != null) { zoom = this.baseLayer.getZoomForExtent(bounds, closest); } return zoom; }, /** * APIMethod: getResolutionForZoom * * Parameter: * zoom - {Float} * * Returns: * {Float} A suitable resolution for the specified zoom. If no baselayer * is set, returns null. */ getResolutionForZoom: function(zoom) { var resolution = null; if(this.baseLayer) { resolution = this.baseLayer.getResolutionForZoom(zoom); } return resolution; }, /** * APIMethod: getZoomForResolution * * Parameter: * 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} A suitable zoom level for the specified resolution. * If no baselayer is set, returns null. */ getZoomForResolution: function(resolution, closest) { var zoom = null; if (this.baseLayer != null) { zoom = this.baseLayer.getZoomForResolution(resolution, closest); } return zoom; }, /********************************************************/ /* */ /* Zooming Functions */ /* */ /* The following functions, all publicly exposed */ /* in the API, are all merely wrappers to the */ /* the setCenter() function */ /* */ /********************************************************/ /** * APIMethod: zoomTo * Zoom to a specific zoom level * * Parameters: * zoom - {Integer} */ zoomTo: function(zoom) { if (this.isValidZoomLevel(zoom)) { this.setCenter(null, zoom); } }, /** * APIMethod: zoomIn * * Parameters: * zoom - {int} */ zoomIn: function() { this.zoomTo(this.getZoom() + 1); }, /** * APIMethod: zoomOut * * Parameters: * zoom - {int} */ zoomOut: function() { this.zoomTo(this.getZoom() - 1); }, /** * APIMethod: zoomToExtent * Zoom to the passed in bounds, recenter * * Parameters: * 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. * */ zoomToExtent: function(bounds, closest) { var center = bounds.getCenterLonLat(); if (this.baseLayer.wrapDateLine) { var maxExtent = this.getMaxExtent(); //fix straddling bounds (in the case of a bbox that straddles the // dateline, it's left and right boundaries will appear backwards. // we fix this by allowing a right value that is greater than the // max value at the dateline -- this allows us to pass a valid // bounds to calculate zoom) // bounds = bounds.clone(); while (bounds.right < bounds.left) { bounds.right += maxExtent.getWidth(); } //if the bounds was straddling (see above), then the center point // we got from it was wrong. So we take our new bounds and ask it // for the center. Because our new bounds is at least partially // outside the bounds of maxExtent, the new calculated center // might also be. We don't want to pass a bad center value to // setCenter, so we have it wrap itself across the date line. // center = bounds.getCenterLonLat().wrapDateLine(maxExtent); } this.setCenter(center, this.getZoomForExtent(bounds, closest)); }, /** * APIMethod: zoomToMaxExtent * Zoom to the full extent and recenter. * * Parameters: * options - * * Allowed Options: * restricted - {Boolean} True to zoom to restricted extent if it is * set. Defaults to true. */ zoomToMaxExtent: function(options) { //restricted is true by default var restricted = (options) ? options.restricted : true; var maxExtent = this.getMaxExtent({ 'restricted': restricted }); this.zoomToExtent(maxExtent); }, /** * APIMethod: zoomToScale * Zoom to a specified scale * * Parameters: * scale - {float} * closest - {Boolean} Find the zoom level that most closely fits the * specified scale. Note that this may result in a zoom that does * not exactly contain the entire extent. * Default is false. * */ zoomToScale: function(scale, closest) { var res = OpenLayers.Util.getResolutionFromScale(scale, this.baseLayer.units); var size = this.getSize(); var w_deg = size.w * res; var h_deg = size.h * res; var center = this.getCenter(); var extent = new OpenLayers.Bounds(center.lon - w_deg / 2, center.lat - h_deg / 2, center.lon + w_deg / 2, center.lat + h_deg / 2); this.zoomToExtent(extent, closest); }, /********************************************************/ /* */ /* Translation Functions */ /* */ /* The following functions translate between */ /* LonLat, LayerPx, and ViewPortPx */ /* */ /********************************************************/ // // TRANSLATION: LonLat <-> ViewPortPx // /** * Method: getLonLatFromViewPortPx * * Parameters: * viewPortPx - {} * * Returns: * {} An OpenLayers.LonLat which is the passed-in view * port , translated into lon/lat * by the current base layer. */ getLonLatFromViewPortPx: function (viewPortPx) { var lonlat = null; if (this.baseLayer != null) { lonlat = this.baseLayer.getLonLatFromViewPortPx(viewPortPx); } return lonlat; }, /** * APIMethod: getViewPortPxFromLonLat * * Parameters: * lonlat - {} * * Returns: * {} An OpenLayers.Pixel which is the passed-in * , translated into view port * pixels by the current base layer. */ getViewPortPxFromLonLat: function (lonlat) { var px = null; if (this.baseLayer != null) { px = this.baseLayer.getViewPortPxFromLonLat(lonlat); } return px; }, // // CONVENIENCE TRANSLATION FUNCTIONS FOR API // /** * APIMethod: getLonLatFromPixel * * Parameters: * px - {} * * Returns: * {} An OpenLayers.LonLat corresponding to the given * OpenLayers.Pixel, translated into lon/lat by the * current base layer */ getLonLatFromPixel: function (px) { return this.getLonLatFromViewPortPx(px); }, /** * APIMethod: getPixelFromLonLat * Returns a pixel location given a map location. The map location is * translated to an integer pixel location (in viewport pixel * coordinates) by the current base layer. * * Parameters: * lonlat - {} A map location. * * Returns: * {} An OpenLayers.Pixel corresponding to the * translated into view port pixels by the current * base layer. */ getPixelFromLonLat: function (lonlat) { var px = this.getViewPortPxFromLonLat(lonlat); px.x = Math.round(px.x); px.y = Math.round(px.y); return px; }, /** * Method: getGeodesicPixelSize * * Parameters: * px - {} The pixel to get the geodesic length for. If * not provided, the center pixel of the map viewport will be used. * * Returns: * {} The geodesic size of the pixel in kilometers. */ getGeodesicPixelSize: function(px) { var lonlat = px ? this.getLonLatFromPixel(px) : (this.getCenter() || new OpenLayers.LonLat(0, 0)); var res = this.getResolution(); var left = lonlat.add(-res / 2, 0); var right = lonlat.add(res / 2, 0); var bottom = lonlat.add(0, -res / 2); var top = lonlat.add(0, res / 2); var dest = new OpenLayers.Projection("EPSG:4326"); var source = this.getProjectionObject() || dest; if(!source.equals(dest)) { left.transform(source, dest); right.transform(source, dest); bottom.transform(source, dest); top.transform(source, dest); } return new OpenLayers.Size( OpenLayers.Util.distVincenty(left, right), OpenLayers.Util.distVincenty(bottom, top) ); }, // // TRANSLATION: ViewPortPx <-> LayerPx // /** * APIMethod: getViewPortPxFromLayerPx * * Parameters: * layerPx - {} * * Returns: * {} Layer Pixel translated into ViewPort Pixel * coordinates */ getViewPortPxFromLayerPx:function(layerPx) { var viewPortPx = null; if (layerPx != null) { var dX = parseInt(this.layerContainerDiv.style.left); var dY = parseInt(this.layerContainerDiv.style.top); viewPortPx = layerPx.add(dX, dY); } return viewPortPx; }, /** * APIMethod: getLayerPxFromViewPortPx * * Parameters: * viewPortPx - {} * * Returns: * {} ViewPort Pixel translated into Layer Pixel * coordinates */ getLayerPxFromViewPortPx:function(viewPortPx) { var layerPx = null; if (viewPortPx != null) { var dX = -parseInt(this.layerContainerDiv.style.left); var dY = -parseInt(this.layerContainerDiv.style.top); layerPx = viewPortPx.add(dX, dY); if (isNaN(layerPx.x) || isNaN(layerPx.y)) { layerPx = null; } } return layerPx; }, // // TRANSLATION: LonLat <-> LayerPx // /** * Method: getLonLatFromLayerPx * * Parameters: * px - {} * * Returns: * {} */ getLonLatFromLayerPx: function (px) { //adjust for displacement of layerContainerDiv px = this.getViewPortPxFromLayerPx(px); return this.getLonLatFromViewPortPx(px); }, /** * APIMethod: getLayerPxFromLonLat * * Parameters: * lonlat - {} lonlat * * Returns: * {} An OpenLayers.Pixel which is the passed-in * , translated into layer pixels * by the current base layer */ getLayerPxFromLonLat: function (lonlat) { //adjust for displacement of layerContainerDiv var px = this.getPixelFromLonLat(lonlat); return this.getLayerPxFromViewPortPx(px); }, CLASS_NAME: "OpenLayers.Map" }); /** * Constant: TILE_WIDTH * {Integer} 256 Default tile width (unless otherwise specified) */ OpenLayers.Map.TILE_WIDTH = 256; /** * Constant: TILE_HEIGHT * {Integer} 256 Default tile height (unless otherwise specified) */ OpenLayers.Map.TILE_HEIGHT = 256;