--- a/labs/openlayers/lib/OpenLayers/Map.js +++ b/labs/openlayers/lib/OpenLayers/Map.js @@ -1,1 +1,2418 @@ - +/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + +/** + * @requires OpenLayers/Util.js + * @requires OpenLayers/Events.js + * @requires OpenLayers/Tween.js + * @requires OpenLayers/Console.js + */ + +/** + * Class: OpenLayers.Map + * Instances of OpenLayers.Map are interactive maps embedded in a web page. + * Create a new map with the <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 + }, + + /** + * 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* {<OpenLayers.Pixel>} 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 + * <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, + + /** + * Property: controls + * {Array(<OpenLayers.Control>)} 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: + * - <OpenLayers.Control.Navigation> + * - <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, + + /** + * 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 + * {<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 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 + * {<OpenLayers.Bounds>} 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 + * {<OpenLayers.Bounds>} + */ + minExtent: null, + + /** + * APIProperty: restrictedExtent + * {<OpenLayers.Bounds>} 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.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 <OpenLayers.Events.on>. 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 + * {<OpenLayers.Bounds>} 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 <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. + * + * 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<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) { + if (OpenLayers.Control != null) { // running full or lite? + this.controls = [ new OpenLayers.Control.Navigation(), + new OpenLayers.Control.PanZoom(), + new OpenLayers.Control.ArgParser(), + new OpenLayers.Control.Attribution() + ]; + } else { + this.controls = []; + } + } + + 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) { + this.addLayers(options.layers); + // set center (and optionally zoom) + if (options.center) { + // zoom can be undefined here + this.setCenter(options.center, options.zoom); + } + } + }, + + /** + * 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.events.attachToElement(this.div); + 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 + */ + 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; + } + + // 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); + } else { + this.events.unregister("resize", this, this.updateSize); + } + + 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.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: + * {<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. + * + * 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(<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. + * + * 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(<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). + * + * 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(<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. + * + * 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(<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). + * + * 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(<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); + }, + + /********************************************************/