diff options
Diffstat (limited to 'misc/openlayers/lib/OpenLayers/Map.js')
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Map.js | 2867 |
1 files changed, 2867 insertions, 0 deletions
diff --git a/misc/openlayers/lib/OpenLayers/Map.js b/misc/openlayers/lib/OpenLayers/Map.js new file mode 100644 index 0000000..56763fa --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Map.js @@ -0,0 +1,2867 @@ +/* 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/Util.js + * @requires OpenLayers/Util/vendorPrefix.js + * @requires OpenLayers/Events.js + * @requires OpenLayers/Tween.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Map + * Instances of OpenLayers.Map are interactive maps embedded in a web page. + * Create a new map with the <OpenLayers.Map> constructor. + * + * On their own maps do not provide much functionality. To extend a map + * it's necessary to add controls (<OpenLayers.Control>) and + * layers (<OpenLayers.Layer>) 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 + }, + + /** + * APIProperty: events + * {<OpenLayers.Events>} + * + * 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 - {<OpenLayers.Pixel>} The pixel location of the event (relative + * to the the map viewport). + * + * 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. When a listener returns "false" the adding will be + * aborted. + * addlayer - triggered after a layer has been added. The event object + * will include a *layer* property that references the added layer. + * preremovelayer - triggered before a layer has been removed. The event + * object will include a *layer* property that references the layer + * to be removed. When a listener returns "false" the removal will be + * aborted. + * 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, visibility change (actual visibility, + * not the layer's visibility property) or attribution change (due to + * extent change). 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, visibility or + * attribution). + * movestart - triggered after the start of a drag, pan, or zoom. The event + * object may include a *zoomChanged* property that tells whether the + * zoom has changed. + * 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 + * updatesize - triggered after the <updateSize> method was executed + */ + + /** + * 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 + * <getResolutionForZoom> instead of layer.resolutions[zoom] as the + * former works for non-integer zoom levels. + */ + fractionalZoom: false, + + /** + * APIProperty: events + * {<OpenLayers.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 <setLayerIndex> or <raiseLayer> 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 <OpenLayers.Map> 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 <render> method. + * + * Note: + * If you are calling <render> after map construction, do not use + * <maxResolution> auto. Instead, divide your <maxExtent> by your + * maximum expected dimension. + */ + div: null, + + /** + * Property: dragging + * {Boolean} The map is currently being dragged. + */ + dragging: false, + + /** + * Property: size + * {<OpenLayers.Size>} Size of the main div (this.div) + */ + size: null, + + /** + * Property: viewPortDiv + * {HTMLDivElement} The element that represents the map viewport + */ + viewPortDiv: null, + + /** + * Property: layerContainerOrigin + * {<OpenLayers.LonLat>} 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(<OpenLayers.Layer>)} Ordered list of layers in the map + */ + layers: null, + + /** + * APIProperty: controls + * {Array(<OpenLayers.Control>)} List of controls associated with the map. + * + * If not provided in the map options at construction, the map will + * by default be given the following controls if present in the build: + * - <OpenLayers.Control.Navigation> or <OpenLayers.Control.TouchNavigation> + * - <OpenLayers.Control.Zoom> or <OpenLayers.Control.PanZoom> + * - <OpenLayers.Control.ArgParser> + * - <OpenLayers.Control.Attribution> + */ + controls: null, + + /** + * Property: popups + * {Array(<OpenLayers.Popup>)} List of popups associated with the map + */ + popups: null, + + /** + * APIProperty: baseLayer + * {<OpenLayers.Layer>} The currently selected base layer. This determines + * min/max zoom level, projection, etc. + */ + baseLayer: null, + + /** + * Property: center + * {<OpenLayers.LonLat>} 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, + + /** + * APIProperty: options + * {Object} The options object passed to the class constructor. Read-only. + */ + options: null, + + // Options + + /** + * APIProperty: tileSize + * {<OpenLayers.Size>} 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 specify the default projection + * for layers added to this map. When using a projection other than EPSG:4326 + * (CRS:84, Geographic) or EPSG:3857 (EPSG:900913, Web Mercator), + * also set maxExtent, maxResolution or resolutions. Default is "EPSG:4326". + * Note that the projection of the map is usually determined + * by that of the current baseLayer (see <baseLayer> and <getProjectionObject>). + */ + projection: "EPSG:4326", + + /** + * APIProperty: units + * {String} The map units. 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: 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} Required if you are not displaying the whole world on a tile + * with the size specified in <tileSize>. + */ + maxResolution: null, + + /** + * APIProperty: minResolution + * {Float} + */ + minResolution: null, + + /** + * APIProperty: maxScale + * {Float} + */ + maxScale: null, + + /** + * APIProperty: minScale + * {Float} + */ + minScale: 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 map. + * Default depends on projection; if this is one of those defined in OpenLayers.Projection.defaults + * (EPSG:4326 or web mercator), maxExtent will be set to the value defined there; + * else, defaults to null. + * To restrict user panning and zooming of the map, use <restrictedExtent> instead. + * The value for <maxExtent> will change calculations for tile URLs. + */ + 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 map. Defaults to null. + */ + minExtent: null, + + /** + * APIProperty: restrictedExtent + * {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * 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 + * {<OpenLayers.Projection>} Requires proj4js support for projections other + * than EPSG:4326 or EPSG:900913/EPSG:3857. 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: tileManager + * {<OpenLayers.TileManager>|Object} By default, and if the build contains + * TileManager.js, the map will use the TileManager to queue image requests + * and to cache tile image elements. To create a map without a TileManager + * configure the map with tileManager: null. To create a TileManager with + * non-default options, supply the options instead or alternatively supply + * an instance of {<OpenLayers.TileManager>}. + */ + + /** + * 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 swallow. + */ + fallThrough: false, + + /** + * APIProperty: autoUpdateSize + * {Boolean} Should OpenLayers automatically update the size of the map + * when the resize event is fired. Default is true. + */ + autoUpdateSize: true, + + /** + * 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, + + /** + * Property: panTween + * {<OpenLayers.Tween>} Animated panning tween object, see panTo() + */ + panTween: 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: zoomTween + * {<OpenLayers.Tween>} Animated zooming tween object, see zoomTo() + */ + zoomTween: null, + + /** + * APIProperty: zoomMethod + * {Function} The Easing function to be used for tweening. Default is + * OpenLayers.Easing.Quad.easeOut. Setting this to 'null' turns off + * animated zooming. + */ + zoomMethod: OpenLayers.Easing.Quad.easeOut, + + /** + * Property: zoomDuration + * {Integer} The number of steps to be passed to the + * OpenLayers.Tween.start() method when the map is zoomed. + * Default is 20. + */ + zoomDuration: 20, + + /** + * Property: paddingForPopups + * {<OpenLayers.Bounds>} Outside margin of the popup. Used to prevent + * the popup from getting too close to the map border. + */ + paddingForPopups : null, + + /** + * Property: layerContainerOriginPx + * {Object} Cached object representing the layer container origin (in pixels). + */ + layerContainerOriginPx: null, + + /** + * Property: minPx + * {Object} An object with a 'x' and 'y' values that is the lower + * left of maxExtent in viewport pixel space. + * Used to verify in moveByPx that the new location we're moving to + * is valid. It is also used in the getLonLatFromViewPortPx function + * of Layer. + */ + minPx: null, + + /** + * Property: maxPx + * {Object} An object with a 'x' and 'y' values that is the top + * right of maxExtent in viewport pixel space. + * Used to verify in moveByPx that the new location we're moving to + * is valid. + */ + maxPx: 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 <div> option is + * provided or if you intend to call the <render> method later. + * options - {Object} Optional object with properties to tag onto the map. + * + * Valid options (in addition to the listed API properties): + * center - {<OpenLayers.LonLat>|Array} The default initial center of the map. + * If provided as array, the first value is the x coordinate, + * and the 2nd value is the y coordinate. + * Only specify if <layers> is provided. + * Note that if an ArgParser/Permalink control is present, + * and the querystring contains coordinates, center will be set + * by that, and this option will be ignored. + * zoom - {Number} The initial zoom level for the map. Only specify if + * <layers> is provided. + * Note that if an ArgParser/Permalink control is present, + * and the querystring contains a zoom level, zoom will be set + * by that, and this option will be ignored. + * extent - {<OpenLayers.Bounds>|Array} The initial extent of the map. + * If provided as an array, the array should consist of + * four values (left, bottom, right, top). + * Only specify if <center> and <zoom> are not provided. + * + * Examples: + * (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 = { + * projection: "EPSG:3857", + * maxExtent: new OpenLayers.Bounds(-200000, -200000, 200000, 200000), + * center: new OpenLayers.LonLat(-12356463.476333, 5621521.4854095) + * }; + * var map = new OpenLayers.Map("map2", options); + * + * // map with non-default options - same as above but with a single argument, + * // a restricted extent, and using arrays for bounds and center + * var map = new OpenLayers.Map({ + * div: "map_id", + * projection: "EPSG:3857", + * maxExtent: [-18924313.432222, -15538711.094146, 18924313.432222, 15538711.094146], + * restrictedExtent: [-13358338.893333, -9608371.5085962, 13358338.893333, 9608371.5085962], + * center: [-12356463.476333, 5621521.4854095] + * }); + * + * // create a map without a reference to a container - call render later + * var map = new OpenLayers.Map({ + * projection: "EPSG:3857", + * maxExtent: new OpenLayers.Bounds(-200000, -200000, 200000, 200000) + * }); + * (end) + */ + 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.paddingForPopups = new OpenLayers.Bounds(15, 15, 15, 15); + + this.theme = OpenLayers._getScriptLocation() + + 'theme/default/style.css'; + + // backup original options + this.options = OpenLayers.Util.extend({}, options); + + // now override default options + OpenLayers.Util.extend(this, options); + + var projCode = this.projection instanceof OpenLayers.Projection ? + this.projection.projCode : this.projection; + OpenLayers.Util.applyDefaults(this, OpenLayers.Projection.defaults[projCode]); + + // allow extents and center to be arrays + if (this.maxExtent && !(this.maxExtent instanceof OpenLayers.Bounds)) { + this.maxExtent = new OpenLayers.Bounds(this.maxExtent); + } + if (this.minExtent && !(this.minExtent instanceof OpenLayers.Bounds)) { + this.minExtent = new OpenLayers.Bounds(this.minExtent); + } + if (this.restrictedExtent && !(this.restrictedExtent instanceof OpenLayers.Bounds)) { + this.restrictedExtent = new OpenLayers.Bounds(this.restrictedExtent); + } + if (this.center && !(this.center instanceof OpenLayers.LonLat)) { + this.center = new OpenLayers.LonLat(this.center); + } + + // 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); + + this.events = new OpenLayers.Events( + this, this.viewPortDiv, null, this.fallThrough, + {includeXY: true} + ); + + if (OpenLayers.TileManager && this.tileManager !== null) { + if (!(this.tileManager instanceof OpenLayers.TileManager)) { + this.tileManager = new OpenLayers.TileManager(this.tileManager); + } + this.tileManager.addMap(this); + } + + // 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.layerContainerOriginPx = {x: 0, y: 0}; + this.applyTransform(); + + this.viewPortDiv.appendChild(this.layerContainerDiv); + + this.updateSize(); + if(this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + + if (this.autoUpdateSize === true) { + // 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<len; ++i) { + if(OpenLayers.Util.isEquivalentUrl(nodes.item(i).href, + this.theme)) { + addNode = false; + break; + } + } + // only add a new node if one with an equivalent url hasn't already + // been added + if(addNode) { + var cssNode = document.createElement('link'); + cssNode.setAttribute('rel', 'stylesheet'); + cssNode.setAttribute('type', 'text/css'); + cssNode.setAttribute('href', this.theme); + document.getElementsByTagName('head')[0].appendChild(cssNode); + } + } + + if (this.controls == null) { // default controls + this.controls = []; + if (OpenLayers.Control != null) { // running full or lite? + // Navigation or TouchNavigation depending on what is in build + if (OpenLayers.Control.Navigation) { + this.controls.push(new OpenLayers.Control.Navigation()); + } else if (OpenLayers.Control.TouchNavigation) { + this.controls.push(new OpenLayers.Control.TouchNavigation()); + } + if (OpenLayers.Control.Zoom) { + this.controls.push(new OpenLayers.Control.Zoom()); + } else if (OpenLayers.Control.PanZoom) { + this.controls.push(new OpenLayers.Control.PanZoom()); + } + + if (OpenLayers.Control.ArgParser) { + this.controls.push(new OpenLayers.Control.ArgParser()); + } + if (OpenLayers.Control.Attribution) { + this.controls.push(new OpenLayers.Control.Attribution()); + } + } + } + + for(var i=0, len=this.controls.length; i<len; i++) { + this.addControlToMap(this.controls[i]); + } + + this.popups = []; + + this.unloadDestroy = OpenLayers.Function.bind(this.destroy, this); + + + // always call map.destroy() + OpenLayers.Event.observe(window, 'unload', this.unloadDestroy); + + // add any initial layers + if (options && options.layers) { + /** + * If you have set options.center, the map center property will be + * set at this point. However, since setCenter has not been called, + * addLayers gets confused. So we delete the map center in this + * case. Because the check below uses options.center, it will + * be properly set below. + */ + delete this.center; + delete this.zoom; + this.addLayers(options.layers); + // set center (and optionally zoom) + if (options.center && !this.getCenter()) { + // zoom can be undefined here + this.setCenter(options.center, options.zoom); + } + } + + if (this.panMethod) { + this.panTween = new OpenLayers.Tween(this.panMethod); + } + if (this.zoomMethod && this.applyTransform.transform) { + this.zoomTween = new OpenLayers.Tween(this.zoomMethod); + } + }, + + /** + * APIMethod: getViewport + * Get the DOMElement representing the view port. + * + * Returns: + * {DOMElement} + */ + getViewport: function() { + return this.viewPortDiv; + }, + + /** + * APIMethod: render + * Render the map to a specified container. + * + * Parameters: + * div - {String|DOMElement} The container that the map should be rendered + * to. If different than the current container, the map viewport + * will be moved from the current to the new container. + */ + render: function(div) { + this.div = OpenLayers.Util.getElement(div); + OpenLayers.Element.addClass(this.div, 'olMap'); + this.viewPortDiv.parentNode.removeChild(this.viewPortDiv); + this.div.appendChild(this.viewPortDiv); + this.updateSize(); + }, + + /** + * Method: unloadDestroy + * Function that is called to destroy the map on page unload. stored here + * so that if map is manually destroyed, we can unregister this. + */ + unloadDestroy: null, + + /** + * Method: updateSizeDestroy + * When the map is destroyed, we need to stop listening to updateSize + * events: this method stores the function we need to unregister in + * non-IE browsers. + */ + updateSizeDestroy: null, + + /** + * APIMethod: destroy + * Destroy this map. + * Note that if you are using an application which removes a container + * of the map from the DOM, you need to ensure that you destroy the + * map *before* this happens; otherwise, the page unload handler + * will fail because the DOM elements that map.destroy() wants + * to clean up will be gone. (See + * http://trac.osgeo.org/openlayers/ticket/2277 for more information). + * This will apply to GeoExt and also to other applications which + * modify the DOM of the container of the OpenLayers Map. + */ + destroy:function() { + // if unloadDestroy is null, we've already been destroyed + if (!this.unloadDestroy) { + return false; + } + + // make sure panning doesn't continue after destruction + if(this.panTween) { + this.panTween.stop(); + this.panTween = null; + } + // make sure zooming doesn't continue after destruction + if(this.zoomTween) { + this.zoomTween.stop(); + this.zoomTween = null; + } + + // map has been destroyed. dont do it again! + OpenLayers.Event.stopObserving(window, 'unload', this.unloadDestroy); + this.unloadDestroy = null; + + if (this.updateSizeDestroy) { + OpenLayers.Event.stopObserving(window, 'resize', + this.updateSizeDestroy); + } + + this.paddingForPopups = null; + + if (this.controls != null) { + for (var i = this.controls.length - 1; 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.viewPortDiv.parentNode) { + this.viewPortDiv.parentNode.removeChild(this.viewPortDiv); + } + this.viewPortDiv = null; + + if (this.tileManager) { + this.tileManager.removeMap(this); + this.tileManager = null; + } + + if(this.eventListeners) { + this.events.un(this.eventListeners); + this.eventListeners = null; + } + this.events.destroy(); + this.events = null; + + this.options = null; + }, + + /** + * APIMethod: setOptions + * Change the map options + * + * Parameters: + * options - {Object} Hashtable of options to tag to the map + */ + setOptions: function(options) { + var updatePxExtent = this.minPx && + options.restrictedExtent != this.restrictedExtent; + OpenLayers.Util.extend(this, options); + // force recalculation of minPx and maxPx + updatePxExtent && this.moveTo(this.getCachedCenter(), this.zoom, { + forceZoomChange: true + }); + }, + + /** + * APIMethod: getTileSize + * Get the tile size for the map + * + * Returns: + * {<OpenLayers.Size>} + */ + 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. + * + * Parameters: + * 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(<OpenLayers.Layer>)} 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. + * + * Parameters: + * 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(<OpenLayers.Layer>)} 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). + * + * Parameters: + * 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(<OpenLayers.Layer>)} 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. + * + * Parameters: + * 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(<OpenLayers.Control>)} 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). + * + * Parameters: + * 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(<OpenLayers.Control>)} 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 + * + * Parameters: + * id - {String} A layer id + * + * Returns: + * {<OpenLayers.Layer>} 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<len; i++) { + var layer = this.layers[i]; + if (layer.id == id) { + foundLayer = layer; + break; + } + } + return foundLayer; + }, + + /** + * Method: setLayerZIndex + * + * Parameters: + * layer - {<OpenLayers.Layer>} + * 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<len; i++) { + var layer = this.layers[i]; + this.setLayerZIndex(layer, i); + } + }, + + /** + * APIMethod: addLayer + * + * Parameters: + * layer - {<OpenLayers.Layer>} + * + * Returns: + * {Boolean} True if the layer has been added to the map. + */ + addLayer: function (layer) { + for(var i = 0, len = this.layers.length; i < len; i++) { + if (this.layers[i] == layer) { + return false; + } + } + if (this.events.triggerEvent("preaddlayer", {layer: layer}) === false) { + return false; + } + if(this.allOverlays) { + layer.isBaseLayer = false; + } + + layer.div.className = "olLayerDiv"; + layer.div.style.overflow = ""; + this.setLayerZIndex(layer, this.layers.length); + + if (layer.isFixed) { + this.viewPortDiv.appendChild(layer.div); + } else { + this.layerContainerDiv.appendChild(layer.div); + } + this.layers.push(layer); + layer.setMap(this); + + if (layer.isBaseLayer || (this.allOverlays && !this.baseLayer)) { + if (this.baseLayer == null) { + // set the first baselaye we add as the baselayer + this.setBaseLayer(layer); + } else { + layer.setVisibility(false); + } + } else { + layer.redraw(); + } + + this.events.triggerEvent("addlayer", {layer: layer}); + layer.events.triggerEvent("added", {map: this, layer: layer}); + layer.afterAdd(); + + return true; + }, + + /** + * APIMethod: addLayers + * + * Parameters: + * layers - {Array(<OpenLayers.Layer>)} + */ + addLayers: function (layers) { + for (var i=0, len=layers.length; i<len; i++) { + this.addLayer(layers[i]); + } + }, + + /** + * APIMethod: removeLayer + * Removes a layer from the map by removing its visual element (the + * layer.div property), then removing it from the map's internal list + * of layers, setting the layer's map property to null. + * + * a "removelayer" event is triggered. + * + * very worthy of mention is that simply removing a layer from a map + * will not cause the removal of any popups which may have been created + * by the layer. this is due to the fact that it was decided at some + * point that popups would not belong to layers. thus there is no way + * for us to know here to which layer the popup belongs. + * + * A simple solution to this is simply to call destroy() on the layer. + * the default OpenLayers.Layer class's destroy() function + * automatically takes care to remove itself from whatever map it has + * been attached to. + * + * The correct solution is for the layer itself to register an + * event-handler on "removelayer" and when it is called, if it + * recognizes itself as the layer being removed, then it cycles through + * its own personal list of popups, removing them from the map. + * + * Parameters: + * layer - {<OpenLayers.Layer>} + * setNewBaseLayer - {Boolean} Default is true + */ + removeLayer: function(layer, setNewBaseLayer) { + if (this.events.triggerEvent("preremovelayer", {layer: layer}) === false) { + return; + } + 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<len; i++) { + var iLayer = this.layers[i]; + if (iLayer.isBaseLayer || this.allOverlays) { + this.setBaseLayer(iLayer); + break; + } + } + } + } + + this.resetLayersZIndex(); + + this.events.triggerEvent("removelayer", {layer: layer}); + layer.events.triggerEvent("removed", {map: this, layer: layer}); + }, + + /** + * APIMethod: getNumLayers + * + * Returns: + * {Int} The number of layers attached to the map. + */ + getNumLayers: function () { + return this.layers.length; + }, + + /** + * APIMethod: getLayerIndex + * + * Parameters: + * layer - {<OpenLayers.Layer>} + * + * 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 - {<OpenLayers.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<len; i++) { + this.setLayerZIndex(this.layers[i], i); + } + this.events.triggerEvent("changelayer", { + layer: layer, property: "order" + }); + if(this.allOverlays) { + if(idx === 0) { + this.setBaseLayer(layer); + } else if(this.baseLayer !== this.layers[0]) { + this.setBaseLayer(this.layers[0]); + } + } + } + }, + + /** + * APIMethod: raiseLayer + * Change the index of the given layer by delta. If delta is positive, + * the layer is moved up the map's layer stack; if delta is negative, + * the layer is moved down. Again, note that this cannot (or at least + * should not) be effectively used to raise base layers above overlays. + * + * Paremeters: + * layer - {<OpenLayers.Layer>} + * 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 - {<OpenLayers.Layer>} + */ + 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.getCachedCenter(); + 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; + + if(!this.allOverlays || this.baseLayer.visibility) { + this.baseLayer.setVisibility(true); + // Layer may previously have been visible but not in range. + // In this case we need to redraw it to make it visible. + if (this.baseLayer.inRange === false) { + this.baseLayer.redraw(); + } + } + + // 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 - {<OpenLayers.Control>} + * px - {<OpenLayers.Pixel>} + */ + 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(<OpenLayers.Control>)} + * pixels - {Array(<OpenLayers.Pixel>)} + */ + addControls: function (controls, pixels) { + var pxs = (arguments.length === 1) ? [] : pixels; + for (var i=0, len=controls.length; i<len; i++) { + var ctrl = controls[i]; + var px = (pxs[i]) ? pxs[i] : null; + this.addControl( ctrl, px ); + } + }, + + /** + * Method: addControlToMap + * + * Parameters: + * + * control - {<OpenLayers.Control>} + * px - {<OpenLayers.Pixel>} + */ + 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: + * {<OpenLayers.Control>} 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<len; i++) { + var control = this.controls[i]; + if (control.id == id) { + returnControl = control; + break; + } + } + return returnControl; + }, + + /** + * APIMethod: removeControl + * Remove a control from the map. Removes the control both from the map + * object's internal array of controls, as well as from the map's + * viewPort (assuming the control was not added outsideViewport) + * + * Parameters: + * control - {<OpenLayers.Control>} 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 - {<OpenLayers.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 - {<OpenLayers.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: + * {<OpenLayers.Size>} An <OpenLayers.Size> 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<len; i++) { + this.layers[i].onMapResize(); + } + + var center = this.getCachedCenter(); + + if (this.baseLayer != null && center != null) { + var zoom = this.getZoom(); + this.zoom = null; + this.setCenter(center, zoom); + } + + } + } + this.events.triggerEvent("updatesize"); + }, + + /** + * Method: getCurrentSize + * + * Returns: + * {<OpenLayers.Size>} A new <OpenLayers.Size> 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 - {<OpenLayers.LonLat>} Default is this.getCenter() + * resolution - {float} Default is this.getResolution() + * + * Returns: + * {<OpenLayers.Bounds>} A bounds based on resolution, center, and + * current mapsize. + */ + calculateBounds: function(center, resolution) { + + var extent = null; + + if (center == null) { + center = this.getCachedCenter(); + } + if (resolution == null) { + resolution = this.getResolution(); + } + + if ((center != null) && (resolution != null)) { + var halfWDeg = (this.size.w * resolution) / 2; + var halfHDeg = (this.size.h * resolution) / 2; + + extent = new OpenLayers.Bounds(center.lon - halfWDeg, + center.lat - halfHDeg, + center.lon + halfWDeg, + center.lat + halfHDeg); + } + + 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: + * {<OpenLayers.LonLat>} + */ + getCenter: function () { + var center = null; + var cachedCenter = this.getCachedCenter(); + if (cachedCenter) { + center = cachedCenter.clone(); + } + return center; + }, + + /** + * Method: getCachedCenter + * + * Returns: + * {<OpenLayers.LonLat>} + */ + getCachedCenter: function() { + if (!this.center && this.size) { + this.center = this.getLonLatFromViewPortPx({ + x: this.size.w / 2, + y: this.size.h / 2 + }); + } + return this.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 + }); + if (options.dragging) { + if (dx != 0 || dy != 0) { + this.moveByPx(dx, dy); + } + } else { + // getCenter + var centerPx = this.getViewPortPxFromLonLat(this.getCachedCenter()); + + // adjust + var newCenterPx = centerPx.add(dx, dy); + + if (this.dragging || !newCenterPx.equals(centerPx)) { + var newCenterLonLat = this.getLonLatFromViewPortPx(newCenterPx); + if (options.animate) { + this.panTo(newCenterLonLat); + } else { + this.moveTo(newCenterLonLat); + if(this.dragging) { + this.dragging = false; + this.events.triggerEvent("moveend"); + } + } + } + } + + }, + + /** + * 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 - {<OpenLayers.LonLat>} + */ + panTo: function(lonlat) { + if (this.panTween && this.getExtent().scale(this.panRatio).containsLonLat(lonlat)) { + var center = this.getCachedCenter(); + + // center will not change, don't do nothing + if (lonlat.equals(center)) { + return; + } + + var from = this.getPixelFromLonLat(center); + var to = this.getPixelFromLonLat(lonlat); + var vector = { x: to.x - from.x, y: to.y - from.y }; + var last = { x: 0, y: 0 }; + + this.panTween.start( { x: 0, y: 0 }, vector, this.panDuration, { + callbacks: { + eachStep: OpenLayers.Function.bind(function(px) { + var x = px.x - last.x, + y = px.y - last.y; + this.moveByPx(x, y); + last.x = Math.round(px.x); + last.y = Math.round(px.y); + }, this), + done: OpenLayers.Function.bind(function(px) { + this.moveTo(lonlat); + this.dragging = false; + this.events.triggerEvent("moveend"); + }, this) + } + }); + } else { + this.setCenter(lonlat); + } + }, + + /** + * APIMethod: setCenter + * Set the map center (and optionally, the zoom level). + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>|Array} The new center location. + * If provided as array, the first value is the x coordinate, + * and the 2nd value is the y coordinate. + * 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) { + if (this.panTween) { + this.panTween.stop(); + } + if (this.zoomTween) { + this.zoomTween.stop(); + } + this.moveTo(lonlat, zoom, { + 'dragging': dragging, + 'forceZoomChange': forceZoomChange + }); + }, + + /** + * Method: moveByPx + * Drag the map by pixels. + * + * Parameters: + * dx - {Number} + * dy - {Number} + */ + moveByPx: function(dx, dy) { + var hw = this.size.w / 2; + var hh = this.size.h / 2; + var x = hw + dx; + var y = hh + dy; + var wrapDateLine = this.baseLayer.wrapDateLine; + var xRestriction = 0; + var yRestriction = 0; + if (this.restrictedExtent) { + xRestriction = hw; + yRestriction = hh; + // wrapping the date line makes no sense for restricted extents + wrapDateLine = false; + } + dx = wrapDateLine || + x <= this.maxPx.x - xRestriction && + x >= this.minPx.x + xRestriction ? Math.round(dx) : 0; + dy = y <= this.maxPx.y - yRestriction && + y >= this.minPx.y + yRestriction ? Math.round(dy) : 0; + if (dx || dy) { + if (!this.dragging) { + this.dragging = true; + this.events.triggerEvent("movestart"); + } + this.center = null; + if (dx) { + this.layerContainerOriginPx.x -= dx; + this.minPx.x -= dx; + this.maxPx.x -= dx; + } + if (dy) { + this.layerContainerOriginPx.y -= dy; + this.minPx.y -= dy; + this.maxPx.y -= dy; + } + this.applyTransform(); + var layer, i, len; + for (i=0, len=this.layers.length; i<len; ++i) { + layer = this.layers[i]; + if (layer.visibility && + (layer === this.baseLayer || layer.inRange)) { + layer.moveByPx(dx, dy); + layer.events.triggerEvent("move"); + } + } + this.events.triggerEvent("move"); + } + }, + + /** + * Method: adjustZoom + * + * Parameters: + * zoom - {Number} The zoom level to adjust + * + * Returns: + * {Integer} Adjusted zoom level that shows a map not wider than its + * <baseLayer>'s maxExtent. + */ + adjustZoom: function(zoom) { + if (this.baseLayer && this.baseLayer.wrapDateLine) { + var resolution, resolutions = this.baseLayer.resolutions, + maxResolution = this.getMaxExtent().getWidth() / this.size.w; + if (this.getResolutionForZoom(zoom) > maxResolution) { + if (this.fractionalZoom) { + zoom = this.getZoomForResolution(maxResolution); + } else { + for (var i=zoom|0, ii=resolutions.length; i<ii; ++i) { + if (resolutions[i] <= maxResolution) { + zoom = i; + break; + } + } + } + } + } + return zoom; + }, + + /** + * APIMethod: getMinZoom + * Returns the minimum zoom level for the current map view. If the base + * layer is configured with <wrapDateLine> set to true, this will be the + * first zoom level that shows no more than one world width in the current + * map viewport. Components that rely on this value (e.g. zoom sliders) + * should also listen to the map's "updatesize" event and call this method + * in the "updatesize" listener. + * + * Returns: + * {Number} Minimum zoom level that shows a map not wider than its + * <baseLayer>'s maxExtent. This is an Integer value, unless the map is + * configured with <fractionalZoom> set to true. + */ + getMinZoom: function() { + return this.adjustZoom(0); + }, + + /** + * Method: moveTo + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} + * zoom - {Integer} + * options - {Object} + */ + moveTo: function(lonlat, zoom, options) { + if (lonlat != null && !(lonlat instanceof OpenLayers.LonLat)) { + lonlat = new OpenLayers.LonLat(lonlat); + } + if (!options) { + options = {}; + } + if (zoom != null) { + zoom = parseFloat(zoom); + if (!this.fractionalZoom) { + zoom = Math.round(zoom); + } + } + var requestedZoom = zoom; + zoom = this.adjustZoom(zoom); + if (zoom !== requestedZoom) { + // zoom was adjusted, so keep old lonlat to avoid panning + lonlat = this.getCenter(); + } + // dragging is false by default + var dragging = options.dragging || this.dragging; + // forceZoomChange is false by default + var forceZoomChange = options.forceZoomChange; + + if (!this.getCachedCenter() && !this.isValidLonLat(lonlat)) { + lonlat = this.maxExtent.getCenterLonLat(); + this.center = lonlat.clone(); + } + + if(this.restrictedExtent != null) { + // In 3.0, decide if we want to change interpretation of maxExtent. + if(lonlat == null) { + lonlat = this.center; + } + 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) { + dragging || this.events.triggerEvent("movestart", { + zoomChanged: zoomChanged + }); + + 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(); + } + + var res = zoomChanged ? + this.getResolutionForZoom(zoom) : this.getResolution(); + // (re)set the layerContainerDiv's location + if (zoomChanged || this.layerContainerOrigin == null) { + this.layerContainerOrigin = this.getCachedCenter(); + this.layerContainerOriginPx.x = 0; + this.layerContainerOriginPx.y = 0; + this.applyTransform(); + var maxExtent = this.getMaxExtent({restricted: true}); + var maxExtentCenter = maxExtent.getCenterLonLat(); + var lonDelta = this.center.lon - maxExtentCenter.lon; + var latDelta = maxExtentCenter.lat - this.center.lat; + var extentWidth = Math.round(maxExtent.getWidth() / res); + var extentHeight = Math.round(maxExtent.getHeight() / res); + this.minPx = { + x: (this.size.w - extentWidth) / 2 - lonDelta / res, + y: (this.size.h - extentHeight) / 2 - latDelta / res + }; + this.maxPx = { + x: this.minPx.x + Math.round(maxExtent.getWidth() / res), + y: this.minPx.y + Math.round(maxExtent.getHeight() / res) + }; + } + + if (zoomChanged) { + this.zoom = zoom; + this.resolution = res; + } + + var bounds = this.getExtent(); + + //send the move call to the baselayer and all the overlays + + if(this.baseLayer.visibility) { + this.baseLayer.moveTo(bounds, zoomChanged, options.dragging); + options.dragging || this.baseLayer.events.triggerEvent( + "moveend", {zoomChanged: zoomChanged} + ); + } + + bounds = this.baseLayer.getExtent(); + + for (var i=this.layers.length-1; i>=0; --i) { + var layer = this.layers[i]; + if (layer !== this.baseLayer && !layer.isBaseLayer) { + var inRange = layer.calculateInRange(); + if (layer.inRange != inRange) { + // the inRange property has changed. If the layer is + // no longer in range, we turn it off right away. If + // the layer is no longer out of range, the moveTo + // call below will turn on the layer. + layer.inRange = inRange; + if (!inRange) { + layer.display(false); + } + this.events.triggerEvent("changelayer", { + layer: layer, property: "visibility" + }); + } + if (inRange && layer.visibility) { + layer.moveTo(bounds, zoomChanged, options.dragging); + options.dragging || layer.events.triggerEvent( + "moveend", {zoomChanged: zoomChanged} + ); + } + } + } + + this.events.triggerEvent("move"); + dragging || this.events.triggerEvent("moveend"); + + if (zoomChanged) { + //redraw popups + for (var i=0, len=this.popups.length; i<len; i++) { + this.popups[i].updatePosition(); + } + this.events.triggerEvent("zoomend"); + } + } + }, + + /** + * Method: centerLayerContainer + * This function takes care to recenter the layerContainerDiv. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} + */ + centerLayerContainer: function (lonlat) { + var originPx = this.getViewPortPxFromLonLat(this.layerContainerOrigin); + var newPx = this.getViewPortPxFromLonLat(lonlat); + + if ((originPx != null) && (newPx != null)) { + var oldLeft = this.layerContainerOriginPx.x; + var oldTop = this.layerContainerOriginPx.y; + var newLeft = Math.round(originPx.x - newPx.x); + var newTop = Math.round(originPx.y - newPx.y); + this.applyTransform( + (this.layerContainerOriginPx.x = newLeft), + (this.layerContainerOriginPx.y = newTop)); + var dx = oldLeft - newLeft; + var dy = oldTop - newTop; + this.minPx.x -= dx; + this.maxPx.x -= dx; + this.minPx.y -= dy; + this.maxPx.y -= dy; + } + }, + + /** + * 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 - {<OpenLayers.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(); + var worldBounds = this.baseLayer.wrapDateLine && maxExtent; + valid = maxExtent.containsLonLat(lonlat, {worldBounds: worldBounds}); + } + 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: + * {<OpenLayers.Projection>} 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: + * {<OpenLayers.Bounds>} 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: + * {<OpenLayers.Bounds>} 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 - {<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} 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 + * + * Parameters: + * 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 + * + * 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} 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. Zooming will be animated unless the map + * is configured with {zoomMethod: null}. To zoom without animation, use + * <setCenter> without a lonlat argument. + * + * Parameters: + * zoom - {Integer} + */ + zoomTo: function(zoom, xy) { + // non-API arguments: + // xy - {<OpenLayers.Pixel>} optional zoom origin + + var map = this; + if (map.isValidZoomLevel(zoom)) { + if (map.baseLayer.wrapDateLine) { + zoom = map.adjustZoom(zoom); + } + if (map.zoomTween) { + var currentRes = map.getResolution(), + targetRes = map.getResolutionForZoom(zoom), + start = {scale: 1}, + end = {scale: currentRes / targetRes}; + if (map.zoomTween.playing && map.zoomTween.duration < 3 * map.zoomDuration) { + // update the end scale, and reuse the running zoomTween + map.zoomTween.finish = { + scale: map.zoomTween.finish.scale * end.scale + }; + } else { + if (!xy) { + var size = map.getSize(); + xy = {x: size.w / 2, y: size.h / 2}; + } + map.zoomTween.start(start, end, map.zoomDuration, { + minFrameRate: 50, // don't spend much time zooming + callbacks: { + eachStep: function(data) { + var containerOrigin = map.layerContainerOriginPx, + scale = data.scale, + dx = ((scale - 1) * (containerOrigin.x - xy.x)) | 0, + dy = ((scale - 1) * (containerOrigin.y - xy.y)) | 0; + map.applyTransform(containerOrigin.x + dx, containerOrigin.y + dy, scale); + }, + done: function(data) { + map.applyTransform(); + var resolution = map.getResolution() / data.scale, + zoom = map.getZoomForResolution(resolution, true) + map.moveTo(map.getZoomTargetCenter(xy, resolution), zoom, true); + } + } + }); + } + } else { + var center = xy ? + map.getZoomTargetCenter(xy, map.getResolutionForZoom(zoom)) : + null; + map.setCenter(center, zoom); + } + } + }, + + /** + * APIMethod: zoomIn + * + */ + zoomIn: function() { + this.zoomTo(this.getZoom() + 1); + }, + + /** + * APIMethod: zoomOut + * + */ + zoomOut: function() { + this.zoomTo(this.getZoom() - 1); + }, + + /** + * APIMethod: zoomToExtent + * Zoom to the passed in bounds, recenter + * + * Parameters: + * bounds - {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * 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) { + if (!(bounds instanceof OpenLayers.Bounds)) { + bounds = new OpenLayers.Bounds(bounds); + } + 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. + // + center = bounds.getCenterLonLat().wrapDateLine(maxExtent); + } + this.setCenter(center, this.getZoomForExtent(bounds, closest)); + }, + + /** + * APIMethod: zoomToMaxExtent + * Zoom to the full extent and recenter. + * + * Parameters: + * options - {Object} + * + * 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 halfWDeg = (this.size.w * res) / 2; + var halfHDeg = (this.size.h * res) / 2; + var center = this.getCachedCenter(); + + var extent = new OpenLayers.Bounds(center.lon - halfWDeg, + center.lat - halfHDeg, + center.lon + halfWDeg, + center.lat + halfHDeg); + this.zoomToExtent(extent, closest); + }, + + /********************************************************/ + /* */ + /* Translation Functions */ + /* */ + /* The following functions translate between */ + /* LonLat, LayerPx, and ViewPortPx */ + /* */ + /********************************************************/ + + // + // TRANSLATION: LonLat <-> ViewPortPx + // + + /** + * Method: 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 current base layer. + */ + getLonLatFromViewPortPx: function (viewPortPx) { + var lonlat = null; + if (this.baseLayer != null) { + lonlat = this.baseLayer.getLonLatFromViewPortPx(viewPortPx); + } + return lonlat; + }, + + /** + * APIMethod: getViewPortPxFromLonLat + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} + * + * Returns: + * {<OpenLayers.Pixel>} An OpenLayers.Pixel which is the passed-in + * <OpenLayers.LonLat>, 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; + }, + + /** + * Method: getZoomTargetCenter + * + * Parameters: + * xy - {<OpenLayers.Pixel>} The zoom origin pixel location on the screen + * resolution - {Float} The resolution we want to get the center for + * + * Returns: + * {<OpenLayers.LonLat>} The location of the map center after the + * transformation described by the origin xy and the target resolution. + */ + getZoomTargetCenter: function (xy, resolution) { + var lonlat = null, + size = this.getSize(), + deltaX = size.w/2 - xy.x, + deltaY = xy.y - size.h/2, + zoomPoint = this.getLonLatFromPixel(xy); + if (zoomPoint) { + lonlat = new OpenLayers.LonLat( + zoomPoint.lon + deltaX * resolution, + zoomPoint.lat + deltaY * resolution + ); + } + return lonlat; + }, + + // + // CONVENIENCE TRANSLATION FUNCTIONS FOR API + // + + /** + * APIMethod: getLonLatFromPixel + * + * Parameters: + * px - {<OpenLayers.Pixel>|Object} An OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * + * Returns: + * {<OpenLayers.LonLat>} 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 - {<OpenLayers.LonLat>} A map location. + * + * Returns: + * {<OpenLayers.Pixel>} An OpenLayers.Pixel corresponding to the + * <OpenLayers.LonLat> 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 - {<OpenLayers.Pixel>} The pixel to get the geodesic length for. If + * not provided, the center pixel of the map viewport will be used. + * + * Returns: + * {<OpenLayers.Size>} The geodesic size of the pixel in kilometers. + */ + getGeodesicPixelSize: function(px) { + var lonlat = px ? this.getLonLatFromPixel(px) : ( + this.getCachedCenter() || 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 - {<OpenLayers.Pixel>} + * + * Returns: + * {<OpenLayers.Pixel>} Layer Pixel translated into ViewPort Pixel + * coordinates + */ + getViewPortPxFromLayerPx:function(layerPx) { + var viewPortPx = null; + if (layerPx != null) { + var dX = this.layerContainerOriginPx.x; + var dY = this.layerContainerOriginPx.y; + viewPortPx = layerPx.add(dX, dY); + } + return viewPortPx; + }, + + /** + * APIMethod: getLayerPxFromViewPortPx + * + * Parameters: + * viewPortPx - {<OpenLayers.Pixel>} + * + * Returns: + * {<OpenLayers.Pixel>} ViewPort Pixel translated into Layer Pixel + * coordinates + */ + getLayerPxFromViewPortPx:function(viewPortPx) { + var layerPx = null; + if (viewPortPx != null) { + var dX = -this.layerContainerOriginPx.x; + var dY = -this.layerContainerOriginPx.y; + layerPx = viewPortPx.add(dX, dY); + if (isNaN(layerPx.x) || isNaN(layerPx.y)) { + layerPx = null; + } + } + return layerPx; + }, + + // + // TRANSLATION: LonLat <-> LayerPx + // + + /** + * Method: getLonLatFromLayerPx + * + * Parameters: + * px - {<OpenLayers.Pixel>} + * + * Returns: + * {<OpenLayers.LonLat>} + */ + getLonLatFromLayerPx: function (px) { + //adjust for displacement of layerContainerDiv + px = this.getViewPortPxFromLayerPx(px); + return this.getLonLatFromViewPortPx(px); + }, + + /** + * APIMethod: getLayerPxFromLonLat + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} lonlat + * + * Returns: + * {<OpenLayers.Pixel>} An OpenLayers.Pixel which is the passed-in + * <OpenLayers.LonLat>, 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); + }, + + /** + * Method: applyTransform + * Applies the given transform to the <layerContainerDiv>. This method has + * a 2-stage fallback from translate3d/scale3d via translate/scale to plain + * style.left/style.top, in which case no scaling is supported. + * + * Parameters: + * x - {Number} x parameter for the translation. Defaults to the x value of + * the map's <layerContainerOriginPx> + * y - {Number} y parameter for the translation. Defaults to the y value of + * the map's <layerContainerOriginPx> + * scale - {Number} scale. Defaults to 1 if not provided. + */ + applyTransform: function(x, y, scale) { + scale = scale || 1; + var origin = this.layerContainerOriginPx, + needTransform = scale !== 1; + x = x || origin.x; + y = y || origin.y; + + var style = this.layerContainerDiv.style, + transform = this.applyTransform.transform, + template = this.applyTransform.template; + + if (transform === undefined) { + transform = OpenLayers.Util.vendorPrefix.style('transform'); + this.applyTransform.transform = transform; + if (transform) { + // Try translate3d, but only if the viewPortDiv has a transform + // defined in a stylesheet + var computedStyle = OpenLayers.Element.getStyle(this.viewPortDiv, + OpenLayers.Util.vendorPrefix.css('transform')); + if (!computedStyle || computedStyle !== 'none') { + template = ['translate3d(', ',0) ', 'scale3d(', ',1)']; + style[transform] = [template[0], '0,0', template[1]].join(''); + } + // If no transform is defined in the stylesheet or translate3d + // does not stick, use translate and scale + if (!template || !~style[transform].indexOf(template[0])) { + template = ['translate(', ') ', 'scale(', ')']; + } + this.applyTransform.template = template; + } + } + + // If we do 3d transforms, we always want to use them. If we do 2d + // transforms, we only use them when we need to. + if (transform !== null && (template[0] === 'translate3d(' || needTransform === true)) { + // Our 2d transforms are combined with style.left and style.top, so + // adjust x and y values and set the origin as left and top + if (needTransform === true && template[0] === 'translate(') { + x -= origin.x; + y -= origin.y; + style.left = origin.x + 'px'; + style.top = origin.y + 'px'; + } + style[transform] = [ + template[0], x, 'px,', y, 'px', template[1], + template[2], scale, ',', scale, template[3] + ].join(''); + } else { + style.left = x + 'px'; + style.top = y + 'px'; + // We previously might have had needTransform, so remove transform + if (transform !== null) { + style[transform] = ''; + } + } + }, + + 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; |